em = static::getContainer()->get(EntityManagerInterface::class); /** @var \PDO $pdo */ $pdo = $this->em->getConnection()->getNativeConnection(); $this->em->getConnection()->beginTransaction(); // sabre builds its request from $_SERVER, which has no REQUEST_METHOD under phpunit $sapi = new class extends Sapi { public static function getRequest(): Request { return new Request('PROPFIND', '/'.DefaultAvailabilityTest::INBOX.'/'); } }; // Same plugin stack as DAVController, reduced to what the property needs $this->server = new Server([new Inbox(new CalendarBackend($pdo), self::PRINCIPAL)], $sapi); $this->server->addPlugin(new PropertyStoragePlugin(new PropertyStorageBackend($pdo))); $this->server->addPlugin(new SchedulePlugin()); $this->server->addPlugin(new DefaultAvailabilityPlugin()); } protected function tearDown(): void { $this->em->getConnection()->rollBack(); parent::tearDown(); } private function propfind(string $property, string $method = 'PROPFIND'): array { $this->server->httpRequest = new Request($method, '/'.self::INBOX.'/'); $result = $this->server->getPropertiesForPath(self::INBOX, [$property], 0); return $result[0]; } public function testAnInboxWithoutAvailabilityGetsAValidDefault(): void { $result = $this->propfind(self::CALDAV_PROPERTY); $this->assertArrayHasKey(self::CALDAV_PROPERTY, $result[200]); $vcalendar = Reader::read($result[200][self::CALDAV_PROPERTY]); $this->assertSame([], $vcalendar->validate(), 'The default availability must be valid iCalendar'); $this->assertNotNull($vcalendar->VAVAILABILITY); $available = $vcalendar->VAVAILABILITY->AVAILABLE; $this->assertNotNull($available); // macOS Calendar only displays a weekly rule whose slot ends on the day it starts $this->assertSame('WEEKLY', $available->RRULE->getParts()['FREQ']); $this->assertSame( $available->DTSTART->getDateTime()->format('Ymd'), $available->DTEND->getDateTime()->format('Ymd') ); // Floating, so that every client reads it in its own time zone $this->assertTrue($available->DTSTART->isFloating()); $this->assertTrue($available->DTEND->isFloating()); } /** * macOS Calendar asks for the pre-RFC 7953 name, which sabre maps onto the CalDAV one. */ public function testTheDefaultIsAlsoServedUnderTheCalendarServerName(): void { $result = $this->propfind(self::CALENDARSERVER_PROPERTY); $this->assertSame(DefaultAvailabilityPlugin::DEFAULT_AVAILABILITY, $result[200][self::CALENDARSERVER_PROPERTY]); } public function testAStoredAvailabilityWins(): void { $stored = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:test\r\nBEGIN:VAVAILABILITY\r\nUID:stored\r\nDTSTAMP:20260101T000000Z\r\nEND:VAVAILABILITY\r\nEND:VCALENDAR\r\n"; $patch = $this->server->updateProperties(self::INBOX, [self::CALDAV_PROPERTY => $stored]); $this->assertSame(200, $patch[self::CALDAV_PROPERTY]); $result = $this->propfind(self::CALDAV_PROPERTY); $this->assertSame($stored, $result[200][self::CALDAV_PROPERTY]); } /** * Free-busy generation reads the property while answering a POST on the outbox: it must * keep seeing nothing, so that users without a stored availability stay always available. */ public function testTheDefaultIsNotServedOutsidePropfind(): void { $result = $this->propfind(self::CALDAV_PROPERTY, 'POST'); $this->assertArrayNotHasKey(self::CALDAV_PROPERTY, $result[200]); $this->assertArrayHasKey(self::CALDAV_PROPERTY, $result[404]); } }