offerings = Mockery::mock(OfferingRepository::class); $this->validator = new WindowValidator($this->offerings); } /** * The reported bug: 5:30–6:00 PM submitted with the length select left on * its 60-minute default. The window fits no lesson, so it used to persist * nothing at all and say nothing. */ public function testRejectsAWindowShorterThanTheLessonLength(): void { $result = $this->validator->validate(3, '2026-09-10 17:30', '2026-09-10 18:00', 60, 0); self::assertInstanceOf(\WP_Error::class, $result); self::assertSame('invalid_window', $result->get_error_code()); // The message names the length actually chosen, so the fix is obvious. self::assertStringContainsString('60-minute', $result->get_error_message()); } public function testAcceptsThatSameWindowAtAFittingLessonLength(): void { $result = $this->validator->validate(3, '2026-09-10 17:30', '2026-09-10 18:00', 30, 0); self::assertInstanceOf(AvailabilitySlot::class, $result); self::assertSame('2026-09-10 17:30:00', $result->startDt); self::assertSame('2026-09-10 18:00:00', $result->endDt); self::assertSame(30, $result->durationMinutes); self::assertNull($result->offeringId); self::assertCount(1, $result->splitByDuration()); } /** @return array */ public static function badDateTimes(): array { return [ 'empty start' => ['', '2026-09-10 18:00'], 'empty end' => ['2026-09-10 17:00', ''], 'unparseable start' => ['tomorrow', '2026-09-10 18:00'], 'unparseable end' => ['2026-09-10 17:00', 'not a date'], ]; } /** @dataProvider badDateTimes */ public function testRejectsUnusableDateTimes(string $start, string $end): void { $result = $this->validator->validate(3, $start, $end, 30, 0); self::assertInstanceOf(\WP_Error::class, $result); self::assertSame('invalid_datetime', $result->get_error_code()); } public function testRejectsAnEndAtOrBeforeTheStart(): void { $backwards = $this->validator->validate(3, '2026-09-10 18:00', '2026-09-10 17:00', 30, 0); $identical = $this->validator->validate(3, '2026-09-10 18:00', '2026-09-10 18:00', 30, 0); self::assertInstanceOf(\WP_Error::class, $backwards); self::assertInstanceOf(\WP_Error::class, $identical); self::assertSame('invalid_datetime', $backwards->get_error_code()); self::assertSame('invalid_datetime', $identical->get_error_code()); } public function testRejectsAWindowSpanningTwoDays(): void { $result = $this->validator->validate(3, '2026-09-10 23:00', '2026-09-11 01:00', 30, 0); self::assertInstanceOf(\WP_Error::class, $result); self::assertSame('invalid_window', $result->get_error_code()); self::assertStringContainsString('same day', $result->get_error_message()); } public function testRejectsAnOfferingOwnedBySomeoneElse(): void { // Instructor 3 posting instructor 9's offering: accepting it would let a // slot inherit another instructor's price and payment routing. $this->offerings->shouldReceive('findById')->once()->with(8) ->andReturn(new Offering(instructorId: 9, title: 'Theirs', kind: Offering::KIND_PRIVATE_LESSON, id: 8)); $result = $this->validator->validate(3, '2026-09-10 17:00', '2026-09-10 18:00', 60, 8); self::assertInstanceOf(\WP_Error::class, $result); self::assertSame('invalid_offering', $result->get_error_code()); } public function testRejectsAnOfferingThatDoesNotExist(): void { $this->offerings->shouldReceive('findById')->once()->with(8)->andReturn(null); $result = $this->validator->validate(3, '2026-09-10 17:00', '2026-09-10 18:00', 60, 8); self::assertInstanceOf(\WP_Error::class, $result); self::assertSame('invalid_offering', $result->get_error_code()); } public function testKeepsAnOfferingTheInstructorOwns(): void { $this->offerings->shouldReceive('findById')->once()->with(8) ->andReturn(new Offering(instructorId: 3, title: 'Mine', kind: Offering::KIND_PRIVATE_LESSON, id: 8)); $result = $this->validator->validate(3, '2026-09-10 17:00', '2026-09-10 18:00', 60, 8); self::assertInstanceOf(AvailabilitySlot::class, $result); self::assertSame(8, $result->offeringId); } public function testFallsBackToTheDefaultLessonLength(): void { $result = $this->validator->validate(3, '2026-09-10 09:00', '2026-09-10 10:00', 0, 0); self::assertInstanceOf(AvailabilitySlot::class, $result); self::assertSame(AvailabilitySlot::DEFAULT_DURATION_MINUTES, $result->durationMinutes); } public function testAcceptsTheDatetimeLocalFormTheFormActuallySubmits(): void { // The browser posts `Y-m-d\TH:i`, not the canonical space-separated form. $result = $this->validator->validate(3, '2026-09-10T17:00', '2026-09-10T18:00', 60, 0); self::assertInstanceOf(AvailabilitySlot::class, $result); self::assertSame('2026-09-10 17:00:00', $result->startDt); } }