test(feature): 補齊五個核心業務流程 Feature test——Offer/Schedule CRUD、Admin 管理、通知觸發、預約列表
Run Tests / test (pull_request) Successful in 1m51s

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>
This commit is contained in:
2026-06-16 18:23:11 +08:00
parent c53e9657b3
commit 49a4b89655
11 changed files with 1220 additions and 0 deletions
@@ -0,0 +1,83 @@
## 0. 前置確認(實作前一次性)
- [x] 0.1 確認 `phpunit.xml``CACHE_STORE=array`Laravel 11 array store 支援 tags,若非 array/redis 須先修正再實作)
- [x] 0.2 確認現有認證模式:所有新測試使用 `$this->actingAs($user)` + `postJson/putJson`,不手動建立 Sanctum token
- [x] 0.3 確認 factory 狀況:`DivingOffer``CourseSchedule``Booking` 均無 factory,一律使用 `Model::create([...])` 直接建立,不新增 factory
## 1. Provider 課程 CRUD 測試
- [x] 1.1 [整合測試] 建立 `tests/Feature/ProviderOfferCrudTest.php`,加入 private helpers
- `makeProvider(): User`User::create + ProviderProfile::createis_verified=true
- `makeOffer(User $provider): DivingOffer`DivingOffer::create 最小必填欄位)
- [x] 1.2 [整合測試] `test_provider_creates_offer_successfully`POST 201data.provider_id = auth provider idDB 存在
- [x] 1.3 [整合測試] `test_create_offer_missing_required_field_returns_422`:缺 title 送出,回傳 422(代表性一個欄位即可)
- [x] 1.4 [整合測試] `test_provider_updates_own_offer`PUT 200DB 欄位已變更
- [x] 1.5 [整合測試] `test_provider_cannot_update_others_offer`Provider B 更新 Provider A 的課程,403
- [x] 1.6 [整合測試] `test_provider_deletes_own_offer`DELETE 200DB 記錄不存在
- [x] 1.7 [整合測試] `test_provider_cannot_delete_others_offer`403
- [x] 1.8 [整合測試] `test_unauthenticated_request_is_rejected`:無 token POST401
## 2. Provider 時段管理測試
- [x] 2.1 [整合測試] 建立 `tests/Feature/ProviderScheduleCrudTest.php`,加入 private helpers
- `makeProvider(): User`
- `makeOffer(User $provider): DivingOffer`
- `makeSchedule(DivingOffer $offer, int $currentParticipants = 0): CourseSchedule`
- [x] 2.2 [整合測試] `test_provider_creates_schedule_successfully`POST 201status=open
- [x] 2.3 [整合測試] `test_provider_cannot_create_schedule_for_others_offer`403
- [x] 2.4 [整合測試] `test_past_date_returns_422`scheduled_date = yesterday422(代表性驗證案例)
- [x] 2.5 [整合測試] `test_provider_updates_schedule_max_participants`PUT 200DB 更新
- [x] 2.6 [整合測試] `test_max_participants_below_current_returns_422`current_participants=3PUT max_participants=2422
- [x] 2.7 [整合測試] `test_provider_cannot_update_others_schedule`403
- [x] 2.8 [整合測試] `test_delete_schedule_marks_active_bookings_as_provider_cancelled`:建立一筆 pending + 一筆 confirmed bookingDELETE 200;斷言時段 status=`cancelled`(記錄保留);兩筆 booking status=`provider_cancelled`(記錄保留,不刪除);不在 pending/confirmed 的 booking 不受影響
## 3. Admin 使用者管理測試
- [x] 3.1 [整合測試] 建立 `tests/Feature/AdminUserManagementTest.php`,加入 private helpers
- `makeAdmin(): User`role=admin
- `makeMember(): User`role=member + MemberProfile
- `makeProvider(bool $isVerified = false): User`role=provider + ProviderProfile
- [x] 3.2 [整合測試] `test_admin_lists_members_only`:建立 member + provider 各一,GET /api/admin/members 回傳 data 只含 member role
- [x] 3.3 [整合測試] `test_admin_lists_providers_only`GET /api/admin/providers 回傳 data 只含 provider role
- [x] 3.4 [整合測試] `test_admin_toggles_member_active_status`:目標 is_active=true → POST toggle-active → DB is_active=false
- [x] 3.5 [整合測試] `test_admin_toggles_provider_verified_status`:目標 is_verified=false → POST toggle-verified → DB is_verified=true(不重複 DivingOfferVisibilityTest 的快取副作用驗證,只斷言 DB 欄位與 HTTP 200
- [x] 3.6 [整合測試] `test_non_admin_is_forbidden`role=member actingAsGET /api/admin/members 回傳 403
## 4. 通知直接觸發路徑測試
收件者依實際 controller 實作(已查閱原始碼 + app/Notifications/ 確認 class 存在):
- `BookingCreatedNotification``MemberBookingController::store``$provider->notify(new BookingCreatedNotification($booking))` → **教練**收到
- `BookingConfirmedNotification``ProviderBookingController::confirm``$booking->member->notify(...)` → **學員**收到
- `BookingRejectedNotification``ProviderBookingController::reject``$booking->member->notify(...)` → **學員**收到
- `BookingCancelledNotification`cancelledBy: 'member'):`MemberBookingController::destroy``$provider->notify(...)` → **教練**收到
- `BookingCancelledNotification`cancelledBy: 'provider'):`ProviderBookingController::cancel``$booking->member->notify(...)` → **學員**收到
- 注意:member/provider cancel 使用**同一個 class** `BookingCancelledNotification``assertSentTo` 只需確認 class 名稱與收件者即可
- [x] 4.1 [整合測試] 建立 `tests/Feature/NotificationTriggerTest.php`setUp 加 `Notification::fake()`;加入 helper 建立 member + provider(含 ProviderProfile + offer + scheduleopen, 未來日期) + pending booking
- [x] 4.2 [整合測試] `test_booking_created_notifies_provider`Member `POST /api/member/bookings``assertSentTo($provider, BookingCreatedNotification::class)`
- [x] 4.3 [整合測試] `test_booking_confirmed_notifies_member`Provider `PUT /api/provider/bookings/{id}/confirm``assertSentTo($member, BookingConfirmedNotification::class)`
- [x] 4.4 [整合測試] `test_booking_rejected_notifies_member`Provider `PUT /api/provider/bookings/{id}/reject``assertSentTo($member, BookingRejectedNotification::class)`
- [x] 4.5 [整合測試] `test_member_cancel_notifies_provider`Member `DELETE /api/member/bookings/{id}`pending, 遠未來日期) → `assertSentTo($provider, BookingCancelledNotification::class)`
- [x] 4.6 [整合測試] `test_provider_cancel_notifies_member`Provider `PUT /api/provider/bookings/{id}/cancel`(先 confirm 再 cancel`assertSentTo($member, BookingCancelledNotification::class)`
## 5. 預約列表端點測試
路由對應(已查閱 routes/api.php + EnsureAdmin middleware 確認):
- `GET /api/member/bookings` → middleware: `auth:sanctum`(無 role 檢查)→ `MemberBookingController::index`,隔離靠 `where member_id = auth()->id()`
- `GET /api/provider/bookings` → middleware: `auth:sanctum`(無 role 檢查)→ `ProviderBookingController::index`,隔離靠 `whereHas schedule.provider_id`
- `GET /api/admin/bookings` → middleware: `auth:sanctum` + `admin``EnsureAdmin` 強制 403)→ `AdminBookingController::index`
- [x] 5.1 [整合測試] 建立 `tests/Feature/BookingListTest.php`,加入 helpers
- `makeProviderWithSchedule(): array`(回傳 [provider, schedule],含 ProviderProfile + DivingOffer + CourseSchedule
- `makePendingBooking(User $member, CourseSchedule $schedule): Booking`
- [x] 5.2 [整合測試] `test_member_sees_only_own_bookings`MemberA 和 MemberB 各建一筆 pending booking(同一 schedule),actingAs MemberA → `GET /api/member/bookings`,斷言 data count=1 且 data[0].id = MemberA booking id
- [x] 5.3 [整合測試] `test_provider_sees_only_own_course_bookings`ProviderA 和 ProviderB 各有一筆 bookingactingAs ProviderA → `GET /api/provider/bookings`,斷言 data 中只含 ProviderA schedule 的 booking id
- [x] 5.4 [整合測試] `test_admin_sees_all_bookings`:建立 2 筆不同 member 的 bookingactingAs admin → `GET /api/admin/bookings`,斷言兩個 booking id 均在 data 中
- [x] 5.5 [整合測試] `test_unauthenticated_request_returns_401`:無 token `GET /api/member/bookings`401
## 6. 驗收
- [x] 6.1 容器內 `php artisan test` 全綠(基準 187 passed,預估完成後 ≥ 230 passed
- [x] 6.2 確認 5 個新測試類別的案例方法數與 spec scenarios 對應(可多不可少)
- [x] 6.3 確認 `phpunit.xml` 仍為 `CACHE_STORE=array`,未被意外改動
- [x] 6.4 將本 change 的 specs 增量套用至主規格 `openspec/specs/feature-test-coverage/spec.md`