test(feature): 補齊五個核心業務流程 Feature test——Offer/Schedule CRUD、Admin 管理、通知觸發、預約列表
Run Tests / test (pull_request) Successful in 1m51s
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:
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\DivingOffer;
|
||||
use App\Models\ProviderProfile;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Provider 課程 CRUD(coach-offers-api 規格)。
|
||||
*
|
||||
* 所有權驗證是核心不變式:provider_id 決定哪位教練可操作,
|
||||
* 未加檢查會讓任何已登入教練刪改他人課程。
|
||||
*/
|
||||
class ProviderOfferCrudTest 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' => 'Test Dive Course',
|
||||
'location' => 'Kenting',
|
||||
'price' => 3000,
|
||||
'region' => '南部',
|
||||
'rating' => 0,
|
||||
'reviews' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── store ────────────────────────────────────────────────
|
||||
|
||||
public function test_provider_creates_offer_successfully(): void
|
||||
{
|
||||
$provider = $this->makeProvider();
|
||||
|
||||
$response = $this->actingAs($provider)->postJson('/api/provider/offers', [
|
||||
'title' => 'New Dive Course',
|
||||
'location' => 'Kenting',
|
||||
'price' => 5000,
|
||||
'region' => '南部',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonPath('data.provider_id', $provider->id);
|
||||
|
||||
$this->assertDatabaseHas('diving_offers', [
|
||||
'title' => 'New Dive Course',
|
||||
'provider_id' => $provider->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_offer_missing_required_field_returns_422(): void
|
||||
{
|
||||
$provider = $this->makeProvider();
|
||||
|
||||
$this->actingAs($provider)->postJson('/api/provider/offers', [
|
||||
// title is missing
|
||||
'location' => 'Kenting',
|
||||
'price' => 5000,
|
||||
'region' => '南部',
|
||||
])->assertStatus(422);
|
||||
}
|
||||
|
||||
public function test_unauthenticated_request_is_rejected(): void
|
||||
{
|
||||
$this->postJson('/api/provider/offers', [
|
||||
'title' => 'New Dive Course',
|
||||
'location' => 'Kenting',
|
||||
'price' => 5000,
|
||||
'region' => '南部',
|
||||
])->assertStatus(401);
|
||||
}
|
||||
|
||||
// ── update ───────────────────────────────────────────────
|
||||
|
||||
public function test_provider_updates_own_offer(): void
|
||||
{
|
||||
$provider = $this->makeProvider();
|
||||
$offer = $this->makeOffer($provider);
|
||||
|
||||
$this->actingAs($provider)->putJson("/api/provider/offers/{$offer->id}", [
|
||||
'title' => 'Updated Title',
|
||||
'price' => 9999,
|
||||
])->assertOk()->assertJsonPath('status', true);
|
||||
|
||||
$this->assertDatabaseHas('diving_offers', [
|
||||
'id' => $offer->id,
|
||||
'title' => 'Updated Title',
|
||||
'price' => 9999,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_provider_cannot_update_others_offer(): void
|
||||
{
|
||||
$ownerProvider = $this->makeProvider();
|
||||
$otherProvider = $this->makeProvider();
|
||||
$offer = $this->makeOffer($ownerProvider);
|
||||
|
||||
$this->actingAs($otherProvider)
|
||||
->putJson("/api/provider/offers/{$offer->id}", ['title' => 'Hacked Title'])
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
// ── destroy ──────────────────────────────────────────────
|
||||
|
||||
public function test_provider_deletes_own_offer(): void
|
||||
{
|
||||
$provider = $this->makeProvider();
|
||||
$offer = $this->makeOffer($provider);
|
||||
|
||||
$this->actingAs($provider)
|
||||
->deleteJson("/api/provider/offers/{$offer->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
|
||||
$this->assertDatabaseMissing('diving_offers', ['id' => $offer->id]);
|
||||
}
|
||||
|
||||
public function test_provider_cannot_delete_others_offer(): void
|
||||
{
|
||||
$ownerProvider = $this->makeProvider();
|
||||
$otherProvider = $this->makeProvider();
|
||||
$offer = $this->makeOffer($ownerProvider);
|
||||
|
||||
$this->actingAs($otherProvider)
|
||||
->deleteJson("/api/provider/offers/{$offer->id}")
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertDatabaseHas('diving_offers', ['id' => $offer->id]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user