test(booking): 補預約核心測試——狀態機/防超賣/Scheduler/聊天授權/Admin 權限邊界
稽核 P2-1:平台核心業務(預約)原本零測試覆蓋。 - BookingLifecycleTest:七狀態機合法/非法轉移、名額帳務、24h 取消截止、授權邊界(17 案例) - BookingOversellTest:confirm 時 lockForUpdate 防超賣不變式、名額釋放回收(3 案例) - BookingSchedulerTest:48h 過期與日期完成的時間邊界(6 案例) - BookingChatAuthTest:HTTP 與 presence channel 雙防線(8 案例) - AdminEndpointAuthTest:非 admin 一律 403、Admin 不可繞過狀態機(5 案例) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Broadcasting\BookingPresenceChannel;
|
||||
use App\Enums\BookingStatus;
|
||||
use App\Enums\ScheduleStatus;
|
||||
use App\Models\Booking;
|
||||
use App\Models\CourseSchedule;
|
||||
use App\Models\DivingOffer;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 預約聊天室授權(booking-chat / user-presence 規格)。
|
||||
*
|
||||
* 聊天內容含個資與交易細節,授權失守等於任意使用者可讀他人對話。
|
||||
* 防線有二:HTTP 端點的參與方檢查、presence channel 的訂閱驗證,
|
||||
* 兩者都必須擋住非參與方與非 confirmed 狀態。
|
||||
*/
|
||||
class BookingChatAuthTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $member;
|
||||
private User $provider;
|
||||
private Booking $booking;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->member = User::factory()->create(['role' => 'member']);
|
||||
$this->provider = User::factory()->create(['role' => 'provider']);
|
||||
|
||||
$offer = DivingOffer::create([
|
||||
'provider_id' => $this->provider->id,
|
||||
'title' => 'Chat Course',
|
||||
'location' => 'Test',
|
||||
'spot' => 'Test Spot',
|
||||
'price' => 1000,
|
||||
'region' => '南部',
|
||||
'rating' => 0,
|
||||
'reviews' => 0,
|
||||
]);
|
||||
|
||||
$schedule = CourseSchedule::create([
|
||||
'diving_offer_id' => $offer->id,
|
||||
'provider_id' => $this->provider->id,
|
||||
'scheduled_date' => now()->addDays(7)->toDateString(),
|
||||
'start_time' => '09:00',
|
||||
'max_participants' => 4,
|
||||
'current_participants' => 1,
|
||||
'status' => ScheduleStatus::Open,
|
||||
]);
|
||||
|
||||
$this->booking = Booking::create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'member_id' => $this->member->id,
|
||||
'participants' => 1,
|
||||
'total_price' => 1000,
|
||||
'status' => BookingStatus::Confirmed,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── HTTP 端點授權 ────────────────────────────────────────
|
||||
|
||||
public function test_participants_can_read_messages_on_confirmed_booking(): void
|
||||
{
|
||||
$this->actingAs($this->member)
|
||||
->getJson("/api/bookings/{$this->booking->id}/messages")
|
||||
->assertOk();
|
||||
|
||||
$this->actingAs($this->provider)
|
||||
->getJson("/api/bookings/{$this->booking->id}/messages")
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
public function test_outsider_member_cannot_read_messages(): void
|
||||
{
|
||||
$outsider = User::factory()->create(['role' => 'member']);
|
||||
|
||||
$this->actingAs($outsider)
|
||||
->getJson("/api/bookings/{$this->booking->id}/messages")
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_other_provider_cannot_read_messages(): void
|
||||
{
|
||||
$otherProvider = User::factory()->create(['role' => 'provider']);
|
||||
|
||||
$this->actingAs($otherProvider)
|
||||
->getJson("/api/bookings/{$this->booking->id}/messages")
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_messages_forbidden_on_pending_booking(): void
|
||||
{
|
||||
$this->booking->update(['status' => BookingStatus::Pending]);
|
||||
|
||||
$this->actingAs($this->member)
|
||||
->getJson("/api/bookings/{$this->booking->id}/messages")
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_outsider_cannot_send_message(): void
|
||||
{
|
||||
$outsider = User::factory()->create(['role' => 'member']);
|
||||
|
||||
$this->actingAs($outsider)
|
||||
->postJson("/api/bookings/{$this->booking->id}/messages", ['body' => 'hi'])
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
// ── Presence channel 訂閱驗證 ────────────────────────────
|
||||
|
||||
public function test_presence_channel_admits_both_participants(): void
|
||||
{
|
||||
$channel = new BookingPresenceChannel();
|
||||
|
||||
$memberResult = $channel->join($this->member, $this->booking);
|
||||
$this->assertIsArray($memberResult);
|
||||
$this->assertSame($this->member->id, $memberResult['user_id']);
|
||||
|
||||
$providerResult = $channel->join($this->provider, $this->booking);
|
||||
$this->assertIsArray($providerResult);
|
||||
$this->assertSame('provider', $providerResult['user_type']);
|
||||
}
|
||||
|
||||
public function test_presence_channel_rejects_outsiders(): void
|
||||
{
|
||||
$channel = new BookingPresenceChannel();
|
||||
|
||||
$this->assertFalse($channel->join(User::factory()->create(['role' => 'member']), $this->booking));
|
||||
$this->assertFalse($channel->join(User::factory()->create(['role' => 'provider']), $this->booking));
|
||||
}
|
||||
|
||||
public function test_presence_channel_rejects_non_confirmed_booking(): void
|
||||
{
|
||||
$this->booking->update(['status' => BookingStatus::Pending]);
|
||||
$this->booking->refresh();
|
||||
|
||||
$channel = new BookingPresenceChannel();
|
||||
|
||||
$this->assertFalse($channel->join($this->member, $this->booking));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user