db = Mockery::mock(\wpdb::class); $this->db->prefix = 'wp_'; $this->repo = new AvailabilityRepository($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_availability', Mockery::on(static function (array $data): bool { return $data['instructor_id'] === 5 && $data['start_dt'] === '2026-04-01 09:00:00' && $data['duration_minutes'] === 30 && $data['offering_id'] === 8 && $data['is_booked'] === 0; }), ['%d', '%d', '%s', '%s', '%d', '%d', '%d', '%s'] ); $this->db->insert_id = 42; $slot = new AvailabilitySlot(5, '2026-04-01 09:00:00', '2026-04-01 10:00:00', 30, 8); $result = $this->repo->insert($slot); self::assertSame(42, $result); } public function testCreateWeeklySeriesInsertsWeeklyAndSharesGroup(): void { Functions\when('current_time')->justReturn('2026-04-07 12:00:00'); $captured = []; $ids = [10, 11, 12]; $this->db->shouldReceive('insert') ->times(3) ->andReturnUsing(function (string $table, array $data) use (&$captured, &$ids): void { $captured[] = $data['start_dt']; $this->db->insert_id = array_shift($ids); }); // The first row is back-filled with its own id as the recurrence group. $this->db->shouldReceive('update') ->once() ->with('wp_us_availability', ['recurrence_group' => 10], ['id' => 10], ['%d'], ['%d']); $first = new AvailabilitySlot(5, '2026-04-07 09:00:00', '2026-04-07 10:00:00', 60); $result = $this->repo->createWeeklySeries($first, 3); self::assertSame([10, 11, 12], $result); self::assertSame( ['2026-04-07 09:00:00', '2026-04-14 09:00:00', '2026-04-21 09:00:00'], $captured ); } public function testFindByIdReturnsNullWhenNotFound(): void { $this->db->shouldReceive('prepare') ->once() ->andReturn('SELECT * FROM wp_us_availability WHERE id = 99'); $this->db->shouldReceive('get_row') ->once() ->andReturn(null); $result = $this->repo->findById(99); self::assertNull($result); } public function testFindByIdReturnsSlotWhenFound(): void { $row = (object) [ 'id' => '10', 'instructor_id' => '5', 'offering_id' => null, 'start_dt' => '2026-04-01 09:00:00', 'end_dt' => '2026-04-01 10:00:00', 'duration_minutes' => '60', 'is_booked' => '0', 'recurrence_group' => null, ]; $this->db->shouldReceive('prepare')->andReturn('SELECT ...'); $this->db->shouldReceive('get_row')->andReturn($row); $slot = $this->repo->findById(10); self::assertInstanceOf(AvailabilitySlot::class, $slot); self::assertSame(10, $slot->id); self::assertSame(5, $slot->instructorId); } public function testMarkBookedUpdatesRecord(): void { $this->db->shouldReceive('update') ->once() ->with('wp_us_availability', ['is_booked' => 1], ['id' => 7], ['%d'], ['%d']) ->andReturn(1); $result = $this->repo->markBooked(7); self::assertTrue($result); } public function testDeleteReturnsFalseWhenRowNotDeleted(): void { $this->db->shouldReceive('delete') ->once() ->with('wp_us_availability', ['id' => 1, 'is_booked' => 0], ['%d', '%d']) ->andReturn(0); self::assertFalse($this->repo->delete(1)); } public function testFindAvailableWithNoFiltersUsesNoParams(): void { $this->db->shouldReceive('get_results') ->once() ->with(Mockery::pattern('/WHERE is_booked = 0/')) ->andReturn([]); $result = $this->repo->findAvailable(); self::assertSame([], $result); } public function testFindAvailableWithInstructorFilterPreparesQuery(): void { $this->db->shouldReceive('prepare') ->once() ->with(Mockery::pattern('/instructor_id = %d/'), Mockery::any()) ->andReturn('SELECT ...'); $this->db->shouldReceive('get_results')->andReturn([]); $this->repo->findAvailable(instructorId: 3); } public function testFindAvailableWithOfferingAndDurationFilters(): void { $this->db->shouldReceive('prepare') ->once() ->with( Mockery::pattern('/offering_id = %d AND duration_minutes = %d/'), Mockery::on(static fn (array $p): bool => $p === [8, 30]) ) ->andReturn('SELECT ...'); $this->db->shouldReceive('get_results')->andReturn([]); $this->repo->findAvailable(offeringId: 8, durationMinutes: 30); } public function testFindByInstructorReturnsSlots(): void { $row = (object) [ 'id' => '5', 'instructor_id' => '3', 'offering_id' => null, 'start_dt' => '2026-04-01 09:00:00', 'end_dt' => '2026-04-01 10:00:00', 'duration_minutes' => '60', 'is_booked' => '0', 'recurrence_group' => null, ]; $this->db->shouldReceive('prepare')->andReturn('SELECT ...'); $this->db->shouldReceive('get_results')->andReturn([$row]); $slots = $this->repo->findByInstructor(3); self::assertCount(1, $slots); self::assertInstanceOf(AvailabilitySlot::class, $slots[0]); } }