db = Mockery::mock(\wpdb::class); $this->db->prefix = 'wp_'; $this->repo = new BookingRepository($this->db); } public function testInsertCallsWpdbInsertAndReturnsId(): void { Functions\expect('current_time')->with('mysql')->andReturn('2026-04-01 12:00:00'); $this->db->shouldReceive('insert') ->once() ->with( 'wp_us_lessons', Mockery::on(static function (array $data): bool { return $data['slot_id'] === 10 && $data['student_id'] === 5 && $data['offering_id'] === 7 && $data['recurrence'] === Lesson::RECURRENCE_SINGLE && $data['status'] === Lesson::STATUS_PENDING; }), ['%d', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%s', '%s'] ); $this->db->insert_id = 77; $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, offeringId: 7); $result = $this->repo->insert($lesson); self::assertSame(77, $result); } public function testInsertSeriesSharesSeriesIdAcrossSlots(): void { Functions\when('current_time')->justReturn('2026-04-01 12:00:00'); $ids = [40, 41, 42]; $this->db->shouldReceive('insert') ->times(3) ->andReturnUsing(function () use (&$ids): void { $this->db->insert_id = array_shift($ids); }); // The first lesson is back-filled with its own id as the series id. $this->db->shouldReceive('update') ->once() ->with('wp_us_lessons', ['series_id' => 40], ['id' => 40], ['%d'], ['%d']); $template = new Lesson(slotId: 0, studentId: 5, instructorId: 3, offeringId: 7); $result = $this->repo->insertSeries($template, [100, 101, 102]); self::assertSame([40, 41, 42], $result); } public function testFindByIdReturnsNullWhenNotFound(): void { $this->db->shouldReceive('prepare')->andReturn('SELECT ...'); $this->db->shouldReceive('get_row')->andReturn(null); self::assertNull($this->repo->findById(99)); } public function testFindByIdReturnsLesson(): void { $row = (object) [ 'id' => '15', 'slot_id' => '10', 'offering_id' => null, 'student_id' => '5', 'instructor_id' => '3', 'recurrence' => Lesson::RECURRENCE_SINGLE, 'series_id' => null, 'status' => 'pending', 'payment_id' => null, 'notes' => null, ]; $this->db->shouldReceive('prepare')->andReturn('SELECT ...'); $this->db->shouldReceive('get_row')->andReturn($row); $lesson = $this->repo->findById(15); self::assertInstanceOf(Lesson::class, $lesson); self::assertSame(15, $lesson->id); } public function testUpdateStatusReturnsFalseForInvalidStatus(): void { $result = $this->repo->updateStatus(1, 'invalid'); self::assertFalse($result); } public function testUpdateStatusCallsWpdbUpdate(): void { $this->db->shouldReceive('update') ->once() ->with( 'wp_us_lessons', ['status' => Lesson::STATUS_CONFIRMED], ['id' => 1], ['%s'], ['%d'] ) ->andReturn(1); self::assertTrue($this->repo->updateStatus(1, Lesson::STATUS_CONFIRMED)); } public function testUpdateStatusReturnsFalseWhenDbFails(): void { $this->db->shouldReceive('update')->andReturn(0); self::assertFalse($this->repo->updateStatus(1, Lesson::STATUS_CONFIRMED)); } public function testCountUpcomingForStudent(): void { Functions\when('current_time')->justReturn('2026-06-08 12:00:00'); $this->db->shouldReceive('prepare') ->once() ->with(Mockery::pattern('/COUNT\(\*\).*l.student_id = %d.*a.start_dt >= %s/s'), 5, Lesson::STATUS_CANCELLED, '2026-06-08 12:00:00') ->andReturn('SELECT ...'); $this->db->shouldReceive('get_var')->andReturn('3'); self::assertSame(3, $this->repo->countUpcomingForStudent(5)); } public function testFindByStudentReturnsLessons(): void { $row = (object) [ 'id' => '1', 'slot_id' => '2', 'offering_id' => null, 'student_id' => '5', 'instructor_id' => '3', 'recurrence' => Lesson::RECURRENCE_SINGLE, 'series_id' => null, 'status' => 'pending', 'payment_id' => null, 'notes' => null, ]; $this->db->shouldReceive('prepare')->andReturn('SELECT ...'); $this->db->shouldReceive('get_results')->andReturn([$row]); $lessons = $this->repo->findByStudent(5); self::assertCount(1, $lessons); self::assertInstanceOf(Lesson::class, $lessons[0]); } }