收口聊天室安全边界并优化特效生命周期
This commit is contained in:
@@ -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