bookings = Mockery::mock(BookingRepository::class); $this->payments = Mockery::mock(PaymentRepository::class); $this->availability = Mockery::mock(AvailabilityRepository::class); $this->offerings = Mockery::mock(OfferingRepository::class); $this->detail = Mockery::mock(LessonDetail::class); $this->controller = new LessonController($this->bookings, $this->payments, $this->availability, $this->offerings, $this->detail); $_POST = []; $_GET = []; Functions\when('current_user_can')->justReturn(true); Functions\when('get_userdata')->justReturn(false); Functions\when('mysql2date')->alias( static fn (string $format, string $date) => date($format, (int) strtotime($date)) ); Functions\when('wp_unslash')->returnArg(); Functions\when('sanitize_text_field')->returnArg(); Functions\when('sanitize_key')->alias(static fn ($v) => strtolower((string) $v)); Functions\when('get_option')->justReturn(1); Functions\when('current_time')->justReturn('2026-07-06'); Functions\when('admin_url')->alias(static fn (string $path) => 'https://example.test/wp-admin/' . $path); Functions\when('add_query_arg')->alias(static fn ($key, $value, $url) => $url . '&' . $key . '=' . $value); } public function testAdminDashboardShowsSlotDateTimeInsteadOfSlotId(): void { $_GET['usc_view'] = 'list'; $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, id: 1); $slot = new AvailabilitySlot( instructorId: 3, startDt: '2026-07-06 09:00:00', endDt: '2026-07-06 10:00:00', id: 10 ); $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); $this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot); $html = $this->render(); self::assertStringContainsString('Jul 6, 2026 9:00 AM–10:00 AM', $html); self::assertStringContainsString('Date/Time', $html); self::assertStringNotContainsString('Slot ID', $html); } public function testSlotCrossingMidnightRepeatsTheDateOnTheEndTime(): void { $_GET['usc_view'] = 'list'; $lesson = new Lesson(slotId: 11, studentId: 5, instructorId: 3, id: 2); $slot = new AvailabilitySlot( instructorId: 3, startDt: '2026-07-06 23:00:00', endDt: '2026-07-07 00:30:00', id: 11 ); $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); $this->availability->shouldReceive('findById')->once()->with(11)->andReturn($slot); $html = $this->render(); self::assertStringContainsString('Jul 6, 2026 11:00 PM–Jul 7, 2026 12:30 AM', $html); } public function testMissingSlotRendersDash(): void { $lesson = new Lesson(slotId: 99, studentId: 5, instructorId: 3, id: 3); $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); $this->availability->shouldReceive('findById')->once()->with(99)->andReturn(null); $html = $this->render(); self::assertStringContainsString('—', $html); } public function testInstructorLessonsShowSlotDateTime(): void { $_GET['usc_view'] = 'list'; Functions\when('get_current_user_id')->justReturn(3); $lesson = new Lesson(slotId: 12, studentId: 5, instructorId: 3, id: 4); $slot = new AvailabilitySlot( instructorId: 3, startDt: '2026-08-01 14:00:00', endDt: '2026-08-01 15:00:00', id: 12 ); $this->bookings->shouldReceive('findUpcomingForInstructor')->once()->with(3)->andReturn([$lesson]); $this->availability->shouldReceive('findById')->once()->with(12)->andReturn($slot); ob_start(); $this->controller->renderInstructorLessons(); $html = (string) ob_get_clean(); self::assertStringContainsString('Aug 1, 2026 2:00 PM–3:00 PM', $html); } public function testDefaultsToWeekViewWithLessonInItsDay(): void { $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, id: 1); $slot = new AvailabilitySlot( instructorId: 3, startDt: '2026-07-08 09:00:00', endDt: '2026-07-08 10:00:00', id: 10 ); $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); $this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot); $html = $this->render(); // Week of Monday 2026-07-06 (start_of_week = 1, today = 2026-07-06). self::assertStringContainsString('Week of Jul 6, 2026', $html); self::assertStringContainsString('Wed Jul 8', $html); self::assertStringContainsString('9:00 AM', $html); // The list table is not rendered in week view. self::assertStringNotContainsString('Date/Time', $html); } public function testWeekViewHonoursRequestedWeek(): void { $_GET['usc_week'] = '2026-08-01'; $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([]); $html = $this->render(); // 2026-08-01 is a Saturday; its Monday-start week begins 2026-07-27. self::assertStringContainsString('Week of Jul 27, 2026', $html); } public function testLessonOutsideDisplayedWeekIsNotShown(): void { $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, id: 1); $slot = new AvailabilitySlot( instructorId: 3, startDt: '2026-09-01 09:00:00', endDt: '2026-09-01 10:00:00', id: 10 ); $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); $this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot); $html = $this->render(); self::assertStringNotContainsString('9:00 AM', $html); } public function testListViewShowsBookedOfferingName(): void { $_GET['usc_view'] = 'list'; $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, offeringId: 8, id: 1); $slot = new AvailabilitySlot( instructorId: 3, startDt: '2026-07-06 09:00:00', endDt: '2026-07-06 10:00:00', id: 10 ); $offering = new \Unsupervised\Schedular\Offering\Offering( instructorId: 3, kind: 'private_lesson', title: 'Piano Lesson', durationMinutes: 60, id: 8 ); $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); $this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot); $this->offerings->shouldReceive('findById')->once()->with(8)->andReturn($offering); $html = $this->render(); self::assertStringContainsString('Piano Lesson', $html); self::assertStringContainsString('lesson_id=1', $html); } public function testLessonIdRoutesToDetailWithAnswersAndPolicies(): void { $_GET['lesson_id'] = '1'; $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, offeringId: 8, id: 1); $slot = new AvailabilitySlot( instructorId: 3, startDt: '2026-07-06 09:00:00', endDt: '2026-07-06 10:00:00', id: 10 ); $offering = new \Unsupervised\Schedular\Offering\Offering( instructorId: 3, kind: 'private_lesson', title: 'Piano Lesson', durationMinutes: 60, id: 8 ); $this->bookings->shouldReceive('findById')->once()->with(1)->andReturn($lesson); $this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot); $this->offerings->shouldReceive('findById')->once()->with(8)->andReturn($offering); // The lesson itself is handed over, so the presenter can follow a series // occurrence back to the anchor its answers and acceptances hang off. $this->detail->shouldReceive('answers')->once()->with($lesson)->andReturn([ ['question' => 'Skill level', 'answer' => 'Beginner'], ]); $this->detail->shouldReceive('acceptances')->once()->with($lesson)->andReturn([ ['policy' => 'Cancellation', 'version' => 'v2', 'accepted_at' => '2026-07-01 10:00:00', 'ip' => '1.2.3.4'], ]); // The list of lessons must never be queried when routing to a detail view. $this->bookings->shouldNotReceive('findAllUpcoming'); $html = $this->render(); self::assertStringContainsString('Lesson details', $html); self::assertStringContainsString('Piano Lesson', $html); self::assertStringContainsString('Skill level', $html); self::assertStringContainsString('Beginner', $html); self::assertStringContainsString('Cancellation', $html); } public function testInstructorCannotOpenAnotherInstructorsLessonDetail(): void { $_GET['lesson_id'] = '1'; Functions\when('get_current_user_id')->justReturn(99); // The lesson belongs to instructor 3, not the current user (99). $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, offeringId: 8, id: 1); $this->bookings->shouldReceive('findById')->once()->with(1)->andReturn($lesson); $this->detail->shouldNotReceive('answers'); $this->detail->shouldNotReceive('acceptances'); ob_start(); $this->controller->renderInstructorLessons(); $html = (string) ob_get_clean(); self::assertStringContainsString('could not be found', $html); self::assertStringNotContainsString('Skill level', $html); } private function render(): string { ob_start(); $this->controller->renderAdminDashboard(); return (string) ob_get_clean(); } }