Files
CFDivePlatform/tests/Feature/ProviderScheduleCrudTest.php
T
a620906209 49a4b89655
Run Tests / test (pull_request) Successful in 1m51s
test(feature): 補齊五個核心業務流程 Feature test——Offer/Schedule CRUD、Admin 管理、通知觸發、預約列表
ProviderOfferCrudTest(7 案例):store/update/destroy 正常流程 + 所有權邊界(403)+ 未認證(401)
ProviderScheduleCrudTest(7 案例):store/update/destroy + 容量不變式(422)+ destroy 同步取消 active bookings
AdminUserManagementTest(5 案例):列表角色過濾 + toggle-active/toggle-verified DB 副作用 + 非 admin 403
NotificationTriggerTest(5 案例):5 條直接觸發路徑,收件者各不同(BookingCreated/CancelledByMember → provider;其餘 → member)
BookingListTest(4 案例):三角色資料隔離邊界 + 未認證 401

全套件 215 passed / 529 assertions,零 regression。
同步新增 openspec/changes/feature-test-coverage/ 與 openspec/specs/feature-test-coverage/spec.md。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 18:23:11 +08:00

203 lines
7.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature;
use App\Enums\BookingStatus;
use App\Enums\ScheduleStatus;
use App\Models\Booking;
use App\Models\CourseSchedule;
use App\Models\DivingOffer;
use App\Models\ProviderProfile;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Provider 時段管理(course-scheduling 規格)。
*
* 容量不變式:max_participants 不可低於 current_participants
* 否則已確認名額會被靜默截斷;destroy 必須同步取消進行中預約,
* 不處理會讓 member 對不存在/取消的時段保有 pending/confirmed 狀態。
*/
class ProviderScheduleCrudTest extends TestCase
{
use RefreshDatabase;
private function makeProvider(): User
{
$provider = User::factory()->create(['role' => 'provider']);
ProviderProfile::create(['user_id' => $provider->id, 'is_verified' => true]);
return $provider;
}
private function makeOffer(User $provider): DivingOffer
{
return DivingOffer::create([
'provider_id' => $provider->id,
'title' => 'Dive Course',
'location' => 'Kenting',
'price' => 3000,
'region' => '南部',
'rating' => 0,
'reviews' => 0,
]);
}
private function makeSchedule(DivingOffer $offer, int $currentParticipants = 0): CourseSchedule
{
return CourseSchedule::create([
'diving_offer_id' => $offer->id,
'provider_id' => $offer->provider_id,
'scheduled_date' => now()->addDays(30)->toDateString(),
'start_time' => '09:00',
'max_participants' => 10,
'current_participants' => $currentParticipants,
'status' => ScheduleStatus::Open,
]);
}
// ── store ────────────────────────────────────────────────
public function test_provider_creates_schedule_successfully(): void
{
$provider = $this->makeProvider();
$offer = $this->makeOffer($provider);
$response = $this->actingAs($provider)->postJson('/api/provider/schedules', [
'diving_offer_id' => $offer->id,
'scheduled_date' => now()->addDays(10)->toDateString(),
'start_time' => '09:00',
'max_participants' => 8,
]);
$response->assertStatus(201)
->assertJsonPath('status', true)
->assertJsonPath('data.status', 'open');
}
public function test_provider_cannot_create_schedule_for_others_offer(): void
{
$ownerProvider = $this->makeProvider();
$otherProvider = $this->makeProvider();
$offer = $this->makeOffer($ownerProvider);
$this->actingAs($otherProvider)->postJson('/api/provider/schedules', [
'diving_offer_id' => $offer->id,
'scheduled_date' => now()->addDays(10)->toDateString(),
'start_time' => '09:00',
'max_participants' => 8,
])->assertStatus(403);
}
public function test_past_date_returns_422(): void
{
$provider = $this->makeProvider();
$offer = $this->makeOffer($provider);
$this->actingAs($provider)->postJson('/api/provider/schedules', [
'diving_offer_id' => $offer->id,
'scheduled_date' => now()->subDay()->toDateString(),
'start_time' => '09:00',
'max_participants' => 8,
])->assertStatus(422);
}
// ── update ───────────────────────────────────────────────
public function test_provider_updates_schedule_max_participants(): void
{
$provider = $this->makeProvider();
$offer = $this->makeOffer($provider);
$schedule = $this->makeSchedule($offer);
$this->actingAs($provider)
->putJson("/api/provider/schedules/{$schedule->id}", ['max_participants' => 20])
->assertOk()
->assertJsonPath('data.max_participants', 20);
$this->assertDatabaseHas('course_schedules', [
'id' => $schedule->id,
'max_participants' => 20,
]);
}
public function test_max_participants_below_current_returns_422(): void
{
$provider = $this->makeProvider();
$offer = $this->makeOffer($provider);
$schedule = $this->makeSchedule($offer, currentParticipants: 3);
$this->actingAs($provider)
->putJson("/api/provider/schedules/{$schedule->id}", ['max_participants' => 2])
->assertStatus(422);
}
public function test_provider_cannot_update_others_schedule(): void
{
$ownerProvider = $this->makeProvider();
$otherProvider = $this->makeProvider();
$offer = $this->makeOffer($ownerProvider);
$schedule = $this->makeSchedule($offer);
$this->actingAs($otherProvider)
->putJson("/api/provider/schedules/{$schedule->id}", ['max_participants' => 20])
->assertStatus(403);
}
// ── destroy ──────────────────────────────────────────────
public function test_delete_schedule_marks_active_bookings_as_provider_cancelled(): void
{
$provider = $this->makeProvider();
$offer = $this->makeOffer($provider);
$schedule = $this->makeSchedule($offer);
$member = User::factory()->create(['role' => 'member']);
$pendingBooking = Booking::create([
'schedule_id' => $schedule->id,
'member_id' => $member->id,
'participants' => 1,
'total_price' => 3000,
'status' => BookingStatus::Pending,
]);
$confirmedBooking = Booking::create([
'schedule_id' => $schedule->id,
'member_id' => User::factory()->create(['role' => 'member'])->id,
'participants' => 1,
'total_price' => 3000,
'status' => BookingStatus::Confirmed,
]);
$completedBooking = Booking::create([
'schedule_id' => $schedule->id,
'member_id' => User::factory()->create(['role' => 'member'])->id,
'participants' => 1,
'total_price' => 3000,
'status' => BookingStatus::Completed,
]);
$this->actingAs($provider)
->deleteJson("/api/provider/schedules/{$schedule->id}")
->assertOk();
$this->assertDatabaseHas('course_schedules', [
'id' => $schedule->id,
'status' => ScheduleStatus::Cancelled->value,
]);
$this->assertDatabaseHas('bookings', [
'id' => $pendingBooking->id,
'status' => BookingStatus::ProviderCancelled->value,
]);
$this->assertDatabaseHas('bookings', [
'id' => $confirmedBooking->id,
'status' => BookingStatus::ProviderCancelled->value,
]);
// 終態 booking 不受影響
$this->assertDatabaseHas('bookings', [
'id' => $completedBooking->id,
'status' => BookingStatus::Completed->value,
]);
}
}