收口聊天室安全边界并优化特效生命周期
This commit is contained in:
@@ -10,6 +10,7 @@ namespace Tests\Feature;
|
||||
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\Department;
|
||||
use App\Models\Gift;
|
||||
use App\Models\Position;
|
||||
use App\Models\Room;
|
||||
use App\Models\Sysparam;
|
||||
@@ -466,6 +467,26 @@ class ChatControllerTest extends TestCase
|
||||
$this->assertTrue($found, 'Message not found in Redis');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试未进入当前房间时不能直接调用发言接口。
|
||||
*/
|
||||
public function test_send_message_requires_user_to_be_in_room(): void
|
||||
{
|
||||
$room = Room::create(['room_name' => 'sndbd']);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
|
||||
'to_user' => '大家',
|
||||
'content' => '跨房间发言',
|
||||
'is_secret' => false,
|
||||
'font_color' => '#000000',
|
||||
'action' => '',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$this->assertSame([], Redis::lrange("room:{$room->id}:messages", 0, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试发送接口会拦截不在白名单内的危险动作值。
|
||||
*/
|
||||
@@ -488,6 +509,57 @@ class ChatControllerTest extends TestCase
|
||||
$response->assertJsonValidationErrors('action');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试发送接口会拒绝非标准十六进制颜色,避免 style 属性注入。
|
||||
*/
|
||||
public function test_send_message_rejects_invalid_font_color(): void
|
||||
{
|
||||
$room = Room::create(['room_name' => 'badcolor']);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get(route('chat.room', $room->id));
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
|
||||
'to_user' => '大家',
|
||||
'content' => '颜色注入测试',
|
||||
'is_secret' => false,
|
||||
'font_color' => '#fff;evil',
|
||||
'action' => '',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors('font_color');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试合法发言颜色仍可正常保存到消息与用户偏好。
|
||||
*/
|
||||
public function test_send_message_accepts_valid_hex_font_color(): void
|
||||
{
|
||||
$room = Room::create(['room_name' => 'goodcolor']);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get(route('chat.room', $room->id));
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
|
||||
'to_user' => '大家',
|
||||
'content' => '合法颜色测试',
|
||||
'is_secret' => false,
|
||||
'font_color' => '#12AaF0',
|
||||
'action' => '',
|
||||
]);
|
||||
|
||||
$response->assertOk()->assertJson(['status' => 'success']);
|
||||
|
||||
$messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
|
||||
$message = collect($messages)
|
||||
->map(fn (string $item) => json_decode($item, true))
|
||||
->first(fn (array $item) => ($item['content'] ?? null) === '合法颜色测试');
|
||||
|
||||
$this->assertSame('#12AaF0', $message['font_color'] ?? null);
|
||||
$this->assertSame('#12AaF0', $user->fresh()->s_color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试定向消息仅广播到发送方与接收方私有频道。
|
||||
*/
|
||||
@@ -634,6 +706,34 @@ class ChatControllerTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试房间公告更新广播中的动态内容会被转义。
|
||||
*/
|
||||
public function test_set_announcement_escapes_html_in_system_message(): void
|
||||
{
|
||||
$room = Room::create(['room_name' => 'annsafe']);
|
||||
$user = $this->createUserWithPositionPermissions([
|
||||
PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->get(route('chat.room', $room->id));
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
|
||||
'announcement' => '<img src=x onerror=alert(1)>',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
|
||||
$systemMessage = collect($messages)
|
||||
->map(fn (string $item) => json_decode($item, true))
|
||||
->first(fn (array $item) => ($item['from_user'] ?? null) === '系统公告');
|
||||
|
||||
$this->assertNotNull($systemMessage);
|
||||
$this->assertStringNotContainsString('<img src=x onerror=alert(1)>', (string) $systemMessage['content']);
|
||||
$this->assertStringContainsString('<img src=x onerror=alert(1)>', (string) $systemMessage['content']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试超过保留期的聊天图片会被命令清理并改成过期占位消息。
|
||||
*/
|
||||
@@ -745,6 +845,9 @@ class ChatControllerTest extends TestCase
|
||||
$receiver = User::factory()->create(['jjb' => 100]);
|
||||
$outsider = User::factory()->create();
|
||||
|
||||
$this->joinRoom($sender, $room);
|
||||
$this->joinRoom($receiver, $room);
|
||||
|
||||
$response = $this->actingAs($sender)->postJson(route('gift.gold'), [
|
||||
'to_user' => $receiver->username,
|
||||
'room_id' => $room->id,
|
||||
@@ -805,6 +908,52 @@ class ChatControllerTest extends TestCase
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试目标用户不在当前房间时不能赠送金币。
|
||||
*/
|
||||
public function test_gift_gold_requires_target_online_in_same_room(): void
|
||||
{
|
||||
$room = Room::create(['room_name' => 'ggbd']);
|
||||
$sender = User::factory()->create(['jjb' => 500]);
|
||||
$receiver = User::factory()->create(['jjb' => 100]);
|
||||
|
||||
$this->joinRoom($sender, $room);
|
||||
|
||||
$response = $this->actingAs($sender)->postJson(route('gift.gold'), [
|
||||
'to_user' => $receiver->username,
|
||||
'room_id' => $room->id,
|
||||
'amount' => 88,
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$this->assertSame(500, $sender->fresh()->jjb);
|
||||
$this->assertSame(100, $receiver->fresh()->jjb);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试目标用户不在当前房间时不能送花。
|
||||
*/
|
||||
public function test_send_flower_requires_target_online_in_same_room(): void
|
||||
{
|
||||
$room = Room::create(['room_name' => 'flwbd']);
|
||||
$sender = User::factory()->create(['jjb' => 500]);
|
||||
$receiver = User::factory()->create(['meili' => 0]);
|
||||
$gift = Gift::query()->where('is_active', true)->firstOrFail();
|
||||
|
||||
$this->joinRoom($sender, $room);
|
||||
|
||||
$response = $this->actingAs($sender)->postJson(route('gift.flower'), [
|
||||
'to_user' => $receiver->username,
|
||||
'room_id' => $room->id,
|
||||
'gift_id' => $gift->id,
|
||||
'count' => 1,
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$this->assertSame(500, $sender->fresh()->jjb);
|
||||
$this->assertSame(0, $receiver->fresh()->meili);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试会员用户首次进房时会把专属欢迎主题写入历史消息。
|
||||
*/
|
||||
@@ -872,6 +1021,8 @@ class ChatControllerTest extends TestCase
|
||||
]);
|
||||
$room = Room::create(['room_name' => 'test_ann', 'room_owner' => 'someone']);
|
||||
|
||||
$this->joinRoom($user, $room);
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
|
||||
'announcement' => 'This is a new test announcement',
|
||||
]);
|
||||
@@ -892,6 +1043,8 @@ class ChatControllerTest extends TestCase
|
||||
]);
|
||||
$room = Room::create(['room_name' => 'test_ann2', 'room_owner' => 'other']);
|
||||
|
||||
$this->joinRoom($user, $room);
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
|
||||
'announcement' => 'This is a new test announcement',
|
||||
]);
|
||||
@@ -969,4 +1122,15 @@ class ChatControllerTest extends TestCase
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将测试用户写入指定房间在线列表,模拟已经进入聊天室的状态。
|
||||
*/
|
||||
private function joinRoom(User $user, Room $room): void
|
||||
{
|
||||
Redis::hset("room:{$room->id}:users", $user->username, json_encode([
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '特效房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
$types = ['sakura', 'meteors', 'gold-rain', 'hearts', 'confetti', 'fireflies'];
|
||||
|
||||
foreach ($types as $type) {
|
||||
@@ -75,6 +76,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '特效权限房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.effect'), [
|
||||
'room_id' => $room->id,
|
||||
@@ -120,6 +122,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '公屏权限房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.announce'), [
|
||||
'room_id' => $room->id,
|
||||
@@ -158,6 +161,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '清屏权限房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
|
||||
Redis::rpush("room:{$room->id}:messages", json_encode([
|
||||
'id' => 1,
|
||||
@@ -188,6 +192,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '刷新房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.refresh_all'), [
|
||||
'room_id' => $room->id,
|
||||
@@ -245,6 +250,8 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '奖励金币房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
$this->joinRoom($target, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.reward'), [
|
||||
'username' => $target->username,
|
||||
@@ -353,6 +360,8 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '职务警告房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
$this->joinRoom($target, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.warn'), [
|
||||
'username' => $target->username,
|
||||
@@ -443,6 +452,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '无权踢人房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.kick'), [
|
||||
'username' => $target->username,
|
||||
@@ -468,6 +478,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '高部门位阶房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.warn'), [
|
||||
'username' => $target->username,
|
||||
@@ -501,6 +512,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '同部门高位阶房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.mute'), [
|
||||
'username' => $target->username,
|
||||
@@ -514,6 +526,114 @@ class AdminCommandControllerTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试有命令权限但未进入目标房间时不能跨房间触发特效。
|
||||
*/
|
||||
public function test_position_user_cannot_run_room_command_without_joining_room(): void
|
||||
{
|
||||
$admin = $this->createPositionedManager([
|
||||
PositionPermissionRegistry::ROOM_FULLSCREEN_EFFECT,
|
||||
]);
|
||||
$room = Room::create([
|
||||
'room_name' => '未进房特效房',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.effect'), [
|
||||
'room_id' => $room->id,
|
||||
'type' => 'fireworks',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403)->assertJson([
|
||||
'status' => 'error',
|
||||
'message' => '请先进入该房间后再执行管理命令',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试目标用户不在当前房间时不能执行用户管理动作。
|
||||
*/
|
||||
public function test_position_user_cannot_moderate_target_outside_current_room(): void
|
||||
{
|
||||
$admin = $this->createPositionedManager([
|
||||
PositionPermissionRegistry::USER_WARN,
|
||||
]);
|
||||
$target = User::factory()->create([
|
||||
'user_level' => 1,
|
||||
]);
|
||||
$room = Room::create([
|
||||
'room_name' => '目标离线房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.warn'), [
|
||||
'username' => $target->username,
|
||||
'room_id' => $room->id,
|
||||
'reason' => '测试',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403)->assertJson([
|
||||
'status' => 'error',
|
||||
'message' => '目标用户不在当前房间,无法执行该操作',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试管理员输入的公告与警告理由会被转义后写入系统消息。
|
||||
*/
|
||||
public function test_admin_command_content_is_escaped_before_broadcasting(): void
|
||||
{
|
||||
Queue::fake();
|
||||
|
||||
[$admin, $target, $room] = $this->createAdminCommandActors();
|
||||
$payload = '<img src=x onerror=alert(1)>';
|
||||
|
||||
$this->actingAs($admin)->postJson(route('command.warn'), [
|
||||
'username' => $target->username,
|
||||
'room_id' => $room->id,
|
||||
'reason' => $payload,
|
||||
])->assertOk();
|
||||
|
||||
$messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
|
||||
$combinedContent = collect($messages)
|
||||
->map(fn (string $item): string => (string) (json_decode($item, true)['content'] ?? ''))
|
||||
->implode("\n");
|
||||
|
||||
$this->assertStringNotContainsString('<img src=x onerror=alert(1)>', $combinedContent);
|
||||
$this->assertStringContainsString('<img src=x onerror=alert(1)>', $combinedContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试没有奖励金币权限的职务,即使配置了额度也不能直接 POST 发奖励。
|
||||
*/
|
||||
public function test_position_user_without_reward_permission_cannot_reward_even_with_limit(): void
|
||||
{
|
||||
$admin = $this->createPositionedManager([
|
||||
PositionPermissionRegistry::USER_WARN,
|
||||
]);
|
||||
$admin->activePosition->position->update([
|
||||
'max_reward' => 100,
|
||||
]);
|
||||
$target = User::factory()->create([
|
||||
'user_level' => 1,
|
||||
]);
|
||||
$room = Room::create([
|
||||
'room_name' => '无奖励权限房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
$this->joinRoom($target, $room);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.reward'), [
|
||||
'username' => $target->username,
|
||||
'room_id' => $room->id,
|
||||
'amount' => 20,
|
||||
]);
|
||||
|
||||
$response->assertStatus(403)->assertJson([
|
||||
'status' => 'error',
|
||||
'message' => '当前职务无权发放奖励',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建管理员命令测试共用的操作者、目标用户和房间。
|
||||
*
|
||||
@@ -534,6 +654,8 @@ class AdminCommandControllerTest extends TestCase
|
||||
$room = Room::create([
|
||||
'room_name' => '管理操作房',
|
||||
]);
|
||||
$this->joinRoom($admin, $room);
|
||||
$this->joinRoom($target, $room);
|
||||
|
||||
return [$admin, $target, $room];
|
||||
}
|
||||
@@ -640,4 +762,15 @@ class AdminCommandControllerTest extends TestCase
|
||||
&& ($item['to_user'] ?? null) === $targetUsername
|
||||
&& str_contains((string) ($item['content'] ?? ''), $needle));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将测试用户写入指定房间在线表,模拟已经进入聊天室的状态。
|
||||
*/
|
||||
private function joinRoom(User $user, Room $room): void
|
||||
{
|
||||
Redis::hset("room:{$room->id}:users", $user->username, json_encode([
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ namespace Tests\Feature;
|
||||
|
||||
use App\Events\EffectBroadcast;
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\Room;
|
||||
use App\Models\ShopItem;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPurchase;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
@@ -24,6 +26,15 @@ class ShopControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 每个测试前清空 Redis,避免房间在线状态串扰。
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Redis::flushall();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试商品列表接口只返回上架商品。
|
||||
*/
|
||||
@@ -86,6 +97,8 @@ class ShopControllerTest extends TestCase
|
||||
public function test_can_buy_one_time_item()
|
||||
{
|
||||
$user = User::factory()->create(['jjb' => 500]);
|
||||
$room = Room::create(['room_name' => '购买房']);
|
||||
$this->joinRoom($user, $room);
|
||||
|
||||
$item = ShopItem::firstOrCreate(
|
||||
['slug' => 'rename_card_test'],
|
||||
@@ -98,7 +111,7 @@ class ShopControllerTest extends TestCase
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => 1,
|
||||
'room_id' => $room->id,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
@@ -121,6 +134,8 @@ class ShopControllerTest extends TestCase
|
||||
public function test_cannot_buy_if_insufficient_funds()
|
||||
{
|
||||
$user = User::factory()->create(['jjb' => 50]);
|
||||
$room = Room::create(['room_name' => '余额不足房']);
|
||||
$this->joinRoom($user, $room);
|
||||
|
||||
$item = ShopItem::firstOrCreate(
|
||||
['slug' => 'rename_card_test'],
|
||||
@@ -133,6 +148,7 @@ class ShopControllerTest extends TestCase
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => $room->id,
|
||||
]);
|
||||
|
||||
$response->assertStatus(400);
|
||||
@@ -150,6 +166,8 @@ class ShopControllerTest extends TestCase
|
||||
public function test_cannot_buy_inactive_item()
|
||||
{
|
||||
$user = User::factory()->create(['jjb' => 500]);
|
||||
$room = Room::create(['room_name' => '下架商品房']);
|
||||
$this->joinRoom($user, $room);
|
||||
|
||||
$item = ShopItem::create([
|
||||
'name' => 'Old Card',
|
||||
@@ -161,6 +179,7 @@ class ShopControllerTest extends TestCase
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => $room->id,
|
||||
]);
|
||||
|
||||
$response->assertStatus(400);
|
||||
@@ -226,6 +245,8 @@ class ShopControllerTest extends TestCase
|
||||
Event::fake([MessageSent::class]);
|
||||
|
||||
$user = User::factory()->create(['jjb' => 500]);
|
||||
$room = Room::create(['room_name' => '自动钓鱼房']);
|
||||
$this->joinRoom($user, $room);
|
||||
|
||||
$item = ShopItem::create([
|
||||
'name' => '自动钓鱼卡(2小时)',
|
||||
@@ -238,14 +259,14 @@ class ShopControllerTest extends TestCase
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => 1,
|
||||
'room_id' => $room->id,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['status' => 'success']);
|
||||
|
||||
Event::assertDispatched(MessageSent::class, function (MessageSent $event) use ($user, $item): bool {
|
||||
return $event->roomId === 1
|
||||
Event::assertDispatched(MessageSent::class, function (MessageSent $event) use ($user, $item, $room): bool {
|
||||
return $event->roomId === $room->id
|
||||
&& ($event->message['from_user'] ?? null) === '钓鱼播报'
|
||||
&& str_contains((string) ($event->message['content'] ?? ''), $user->username)
|
||||
&& str_contains((string) ($event->message['content'] ?? ''), $item->name);
|
||||
@@ -263,6 +284,8 @@ class ShopControllerTest extends TestCase
|
||||
'username' => 'buyer-user',
|
||||
'jjb' => 5000,
|
||||
]);
|
||||
$room = Room::create(['room_name' => '指定特效房']);
|
||||
$this->joinRoom($buyer, $room);
|
||||
$recipient = User::factory()->create([
|
||||
'username' => 'receiver-user',
|
||||
]);
|
||||
@@ -278,7 +301,7 @@ class ShopControllerTest extends TestCase
|
||||
|
||||
$response = $this->actingAs($buyer)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => 1,
|
||||
'room_id' => $room->id,
|
||||
'recipient' => $recipient->username,
|
||||
'message' => '送你一场烟花',
|
||||
]);
|
||||
@@ -291,8 +314,8 @@ class ShopControllerTest extends TestCase
|
||||
'gift_message' => '送你一场烟花',
|
||||
]);
|
||||
|
||||
Event::assertDispatched(EffectBroadcast::class, function (EffectBroadcast $event) use ($buyer, $recipient, $item): bool {
|
||||
return $event->roomId === 1
|
||||
Event::assertDispatched(EffectBroadcast::class, function (EffectBroadcast $event) use ($buyer, $recipient, $item, $room): bool {
|
||||
return $event->roomId === $room->id
|
||||
&& $event->type === $item->effectKey()
|
||||
&& $event->operator === $buyer->username
|
||||
&& $event->targetUsername === $recipient->username
|
||||
@@ -301,12 +324,114 @@ class ShopControllerTest extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试定向特效的目标用户名作为标识字段时保持原始值,避免前端比较失败。
|
||||
*/
|
||||
public function test_buy_instant_effect_keeps_raw_target_username_identifier(): void
|
||||
{
|
||||
Event::fake([EffectBroadcast::class]);
|
||||
|
||||
$buyer = User::factory()->create([
|
||||
'username' => 'buyer-user-raw',
|
||||
'jjb' => 5000,
|
||||
]);
|
||||
$room = Room::create(['room_name' => '原始目标名房']);
|
||||
$this->joinRoom($buyer, $room);
|
||||
$recipient = User::factory()->create([
|
||||
'username' => 'receiver&user',
|
||||
]);
|
||||
$item = ShopItem::create([
|
||||
'name' => '烟花单次卡',
|
||||
'slug' => 'once_fireworks_raw_target',
|
||||
'type' => 'instant',
|
||||
'price' => 888,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($buyer)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => $room->id,
|
||||
'recipient' => $recipient->username,
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('target_username', $recipient->username);
|
||||
|
||||
Event::assertDispatched(EffectBroadcast::class, function (EffectBroadcast $event) use ($recipient): bool {
|
||||
return $event->targetUsername === $recipient->username;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试购买特效时缺少房间 ID 会被拒绝,避免广播到 room.0。
|
||||
*/
|
||||
public function test_buy_instant_effect_requires_room_id(): void
|
||||
{
|
||||
$buyer = User::factory()->create([
|
||||
'jjb' => 5000,
|
||||
]);
|
||||
$item = ShopItem::create([
|
||||
'name' => '烟花单次卡',
|
||||
'slug' => 'once_fireworks_missing_room',
|
||||
'type' => 'instant',
|
||||
'price' => 888,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($buyer)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试购买特效的赠言会转义后再进入广播载荷。
|
||||
*/
|
||||
public function test_buy_instant_effect_escapes_gift_message(): void
|
||||
{
|
||||
Event::fake([EffectBroadcast::class, MessageSent::class]);
|
||||
|
||||
$buyer = User::factory()->create([
|
||||
'jjb' => 5000,
|
||||
]);
|
||||
$room = Room::create(['room_name' => '赠言安全房']);
|
||||
$this->joinRoom($buyer, $room);
|
||||
$item = ShopItem::create([
|
||||
'name' => '烟花单次卡',
|
||||
'slug' => 'once_fireworks_safe_message',
|
||||
'type' => 'instant',
|
||||
'price' => 888,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($buyer)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => $room->id,
|
||||
'recipient' => 'all',
|
||||
'message' => '<img src=x onerror=alert(1)>',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('gift_message', '<img src=x onerror=alert(1)>');
|
||||
|
||||
Event::assertDispatched(EffectBroadcast::class, function (EffectBroadcast $event): bool {
|
||||
return $event->giftMessage === '<img src=x onerror=alert(1)>';
|
||||
});
|
||||
Event::assertDispatched(MessageSent::class, function (MessageSent $event): bool {
|
||||
return str_contains((string) ($event->message['content'] ?? ''), '<img src=x onerror=alert(1)>')
|
||||
&& ! str_contains((string) ($event->message['content'] ?? ''), '<img src=x onerror=alert(1)>');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试购买签到补签卡会扣金币并生成可用背包记录。
|
||||
*/
|
||||
public function test_buy_sign_repair_card_creates_active_purchase(): void
|
||||
{
|
||||
$user = User::factory()->create(['jjb' => 12000]);
|
||||
$room = Room::create(['room_name' => '补签卡房']);
|
||||
$this->joinRoom($user, $room);
|
||||
$item = ShopItem::query()->where('slug', 'sign_repair_card')->firstOrFail();
|
||||
$item->update([
|
||||
'price' => 10000,
|
||||
@@ -315,6 +440,7 @@ class ShopControllerTest extends TestCase
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => $room->id,
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
@@ -335,6 +461,8 @@ class ShopControllerTest extends TestCase
|
||||
public function test_buy_sign_repair_card_supports_quantity(): void
|
||||
{
|
||||
$user = User::factory()->create(['jjb' => 35000]);
|
||||
$room = Room::create(['room_name' => '多张补签卡房']);
|
||||
$this->joinRoom($user, $room);
|
||||
$item = ShopItem::query()->where('slug', 'sign_repair_card')->firstOrFail();
|
||||
$item->update([
|
||||
'price' => 10000,
|
||||
@@ -343,6 +471,7 @@ class ShopControllerTest extends TestCase
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('shop.buy'), [
|
||||
'item_id' => $item->id,
|
||||
'room_id' => $room->id,
|
||||
'quantity' => 3,
|
||||
]);
|
||||
|
||||
@@ -358,4 +487,15 @@ class ShopControllerTest extends TestCase
|
||||
->where('status', 'active')
|
||||
->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将测试用户写入指定房间在线表,模拟已经进入聊天室的状态。
|
||||
*/
|
||||
private function joinRoom(User $user, Room $room): void
|
||||
{
|
||||
Redis::hset("room:{$room->id}:users", $user->username, json_encode([
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user