instructorId); self::assertSame('2026-04-01 09:00:00', $slot->startDt); self::assertSame(30, $slot->durationMinutes); self::assertSame(8, $slot->offeringId); self::assertSame(100, $slot->recurrenceGroup); self::assertFalse($slot->isBooked); self::assertSame(42, $slot->id); } public function testDefaults(): void { $slot = new AvailabilitySlot(1, '2026-04-01 09:00:00', '2026-04-01 10:00:00'); self::assertSame(60, $slot->durationMinutes); self::assertNull($slot->offeringId); self::assertFalse($slot->isBooked); self::assertNull($slot->recurrenceGroup); self::assertNull($slot->id); } public function testFromRowMapsCorrectlyAndCastsNullables(): void { $row = (object) [ 'id' => '7', 'instructor_id' => '3', 'offering_id' => null, 'start_dt' => '2026-05-10 14:00:00', 'end_dt' => '2026-05-10 15:00:00', 'duration_minutes' => '60', 'is_booked' => '1', 'recurrence_group' => '7', ]; $slot = AvailabilitySlot::fromRow($row); self::assertSame(7, $slot->id); self::assertSame(3, $slot->instructorId); self::assertNull($slot->offeringId); self::assertSame(60, $slot->durationMinutes); self::assertSame(7, $slot->recurrenceGroup); self::assertTrue($slot->isBooked); } public function testNormalizeDateTimeAcceptsCanonicalAndDatetimeLocalForms(): void { self::assertSame('2026-04-01 09:00:00', AvailabilitySlot::normalizeDateTime('2026-04-01 09:00:00')); self::assertSame('2026-04-01 09:00:00', AvailabilitySlot::normalizeDateTime('2026-04-01 09:00')); self::assertSame('2026-04-01 09:00:00', AvailabilitySlot::normalizeDateTime('2026-04-01T09:00')); self::assertSame('2026-04-01 09:00:30', AvailabilitySlot::normalizeDateTime('2026-04-01T09:00:30')); } public function testNormalizeDateTimeRejectsGarbageAndImpossibleDates(): void { self::assertNull(AvailabilitySlot::normalizeDateTime('')); self::assertNull(AvailabilitySlot::normalizeDateTime('not a date')); self::assertNull(AvailabilitySlot::normalizeDateTime('next tuesday')); self::assertNull(AvailabilitySlot::normalizeDateTime('2026-04-01')); self::assertNull(AvailabilitySlot::normalizeDateTime('2026-13-01 09:00:00')); self::assertNull(AvailabilitySlot::normalizeDateTime('2026-02-30 09:00:00')); self::assertNull(AvailabilitySlot::normalizeDateTime('2026-04-01 25:00:00')); self::assertNull(AvailabilitySlot::normalizeDateTime("2026-04-01 09:00:00'); DROP TABLE x;--")); } public function testToArrayContainsExpectedKeys(): void { $slot = new AvailabilitySlot(1, '2026-04-01 09:00:00', '2026-04-01 10:00:00', 30, 8, false, null, 10); $arr = $slot->toArray(); foreach (['id', 'instructor_id', 'offering_id', 'start_dt', 'end_dt', 'duration_minutes', 'is_booked', 'recurrence_group'] as $key) { self::assertArrayHasKey($key, $arr); } } }