diff --git a/tests/Unit/Enums/BookingStatusTest.php b/tests/Unit/Enums/BookingStatusTest.php new file mode 100644 index 0000000..6824f7c --- /dev/null +++ b/tests/Unit/Enums/BookingStatusTest.php @@ -0,0 +1,67 @@ + $case->value, BookingStatus::cases()); + + $this->assertEqualsCanonicalizing([ + 'pending', + 'confirmed', + 'completed', + 'rejected', + 'expired', + 'member_cancelled', + 'provider_cancelled', + ], $values); + } + + /** @dataProvider validStringProvider */ + public function test_from_returns_correct_case(string $value, BookingStatus $expected): void + { + $this->assertSame($expected, BookingStatus::from($value)); + } + + public static function validStringProvider(): array + { + return [ + ['pending', BookingStatus::Pending], + ['confirmed', BookingStatus::Confirmed], + ['completed', BookingStatus::Completed], + ['rejected', BookingStatus::Rejected], + ['expired', BookingStatus::Expired], + ['member_cancelled', BookingStatus::MemberCancelled], + ['provider_cancelled', BookingStatus::ProviderCancelled], + ]; + } + + public function test_try_from_returns_null_for_invalid_value(): void + { + $this->assertNull(BookingStatus::tryFrom('invalid_status')); + $this->assertNull(BookingStatus::tryFrom('')); + $this->assertNull(BookingStatus::tryFrom('PENDING')); + } + + public function test_valid_transitions_covers_all_cases(): void + { + $allCases = array_map(fn ($case) => $case->value, BookingStatus::cases()); + $transitionKeys = array_keys(\App\Models\Booking::VALID_TRANSITIONS); + + $this->assertEqualsCanonicalizing( + $allCases, + $transitionKeys, + 'VALID_TRANSITIONS 必須包含所有 BookingStatus 狀態,避免未知狀態繞過狀態機' + ); + } +} diff --git a/tests/Unit/Models/BookingTransitionTest.php b/tests/Unit/Models/BookingTransitionTest.php new file mode 100644 index 0000000..e3a72b6 --- /dev/null +++ b/tests/Unit/Models/BookingTransitionTest.php @@ -0,0 +1,155 @@ +status = $status; + return $booking; + } + + // ── pending 合法轉換 ───────────────────────────────────── + + public function test_pending_can_transition_to_confirmed(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::Confirmed) + ); + } + + public function test_pending_can_transition_to_rejected(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::Rejected) + ); + } + + public function test_pending_can_transition_to_expired(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::Expired) + ); + } + + public function test_pending_can_transition_to_member_cancelled(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::MemberCancelled) + ); + } + + // ── pending 非法轉換 ───────────────────────────────────── + + public function test_pending_cannot_transition_to_completed(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::Completed) + ); + } + + public function test_pending_cannot_transition_to_provider_cancelled(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::ProviderCancelled) + ); + } + + // ── confirmed 合法轉換 ─────────────────────────────────── + + public function test_confirmed_can_transition_to_completed(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::Completed) + ); + } + + public function test_confirmed_can_transition_to_member_cancelled(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::MemberCancelled) + ); + } + + public function test_confirmed_can_transition_to_provider_cancelled(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::ProviderCancelled) + ); + } + + // ── confirmed 非法轉換 ─────────────────────────────────── + + public function test_confirmed_cannot_transition_to_pending(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::Pending) + ); + } + + public function test_confirmed_cannot_transition_to_rejected(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::Rejected) + ); + } + + public function test_confirmed_cannot_transition_to_expired(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::Expired) + ); + } + + // ── 終態:不可再轉換 ───────────────────────────────────── + + /** @dataProvider terminalStatuses */ + public function test_terminal_status_cannot_transition_to_any_state(BookingStatus $terminal): void + { + $booking = $this->bookingWithStatus($terminal); + + foreach (BookingStatus::cases() as $target) { + $this->assertFalse( + $booking->canTransitionTo($target), + "終態 {$terminal->value} 不應能轉換至 {$target->value}" + ); + } + } + + public static function terminalStatuses(): array + { + return [ + 'completed' => [BookingStatus::Completed], + 'rejected' => [BookingStatus::Rejected], + 'expired' => [BookingStatus::Expired], + 'member_cancelled' => [BookingStatus::MemberCancelled], + 'provider_cancelled' => [BookingStatus::ProviderCancelled], + ]; + } +} diff --git a/tests/Unit/Traits/NormalizesEmailTest.php b/tests/Unit/Traits/NormalizesEmailTest.php new file mode 100644 index 0000000..c24d575 --- /dev/null +++ b/tests/Unit/Traits/NormalizesEmailTest.php @@ -0,0 +1,57 @@ +subject = new class { + use NormalizesEmail; + + public function normalize(string $email): string + { + return $this->normalizeEmail($email); + } + }; + } + + public function test_uppercase_is_lowercased(): void + { + $this->assertSame('user@example.com', $this->subject->normalize('USER@EXAMPLE.COM')); + } + + public function test_leading_and_trailing_whitespace_is_trimmed(): void + { + $this->assertSame('user@example.com', $this->subject->normalize(' user@example.com ')); + } + + public function test_mixed_case_and_whitespace_are_both_normalized(): void + { + $this->assertSame('user@example.com', $this->subject->normalize(' User@Example.COM ')); + } + + public function test_already_normalized_email_is_unchanged(): void + { + $this->assertSame('user@example.com', $this->subject->normalize('user@example.com')); + } + + public function test_tab_whitespace_is_trimmed(): void + { + $this->assertSame('user@example.com', $this->subject->normalize("\tuser@example.com\t")); + } +}