Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>pull/53311/head
parent
10852e01be
commit
4a6909ffef
@ -0,0 +1,13 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
|
||||
namespace OCA\DAV\Exception; |
||||
|
||||
class ExampleEventException extends \Exception { |
||||
} |
||||
@ -0,0 +1,31 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
|
||||
namespace OCA\DAV\Model; |
||||
|
||||
use Sabre\VObject\Component\VCalendar; |
||||
|
||||
/** |
||||
* Simple DTO to store a parsed example event and its UID. |
||||
*/ |
||||
final class ExampleEvent { |
||||
public function __construct( |
||||
private readonly VCalendar $vCalendar, |
||||
private readonly string $uid, |
||||
) { |
||||
} |
||||
|
||||
public function getUid(): string { |
||||
return $this->uid; |
||||
} |
||||
|
||||
public function getIcs(): string { |
||||
return $this->vCalendar->serialize(); |
||||
} |
||||
} |
||||
@ -0,0 +1,205 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
|
||||
namespace OCA\DAV\Service; |
||||
|
||||
use OCA\DAV\AppInfo\Application; |
||||
use OCA\DAV\CalDAV\CalDavBackend; |
||||
use OCA\DAV\Exception\ExampleEventException; |
||||
use OCA\DAV\Model\ExampleEvent; |
||||
use OCP\AppFramework\Utility\ITimeFactory; |
||||
use OCP\Files\IAppData; |
||||
use OCP\Files\NotFoundException; |
||||
use OCP\Files\NotPermittedException; |
||||
use OCP\IAppConfig; |
||||
use OCP\IL10N; |
||||
use OCP\Security\ISecureRandom; |
||||
use Sabre\VObject\Component\VCalendar; |
||||
use Sabre\VObject\Component\VEvent; |
||||
|
||||
class ExampleEventService { |
||||
private const FOLDER_NAME = 'example_event'; |
||||
private const FILE_NAME = 'example_event.ics'; |
||||
private const ENABLE_CONFIG_KEY = 'create_example_event'; |
||||
|
||||
public function __construct( |
||||
private readonly CalDavBackend $calDavBackend, |
||||
private readonly ISecureRandom $random, |
||||
private readonly ITimeFactory $time, |
||||
private readonly IAppData $appData, |
||||
private readonly IAppConfig $appConfig, |
||||
private readonly IL10N $l10n, |
||||
) { |
||||
} |
||||
|
||||
public function createExampleEvent(int $calendarId): void { |
||||
if (!$this->shouldCreateExampleEvent()) { |
||||
return; |
||||
} |
||||
|
||||
$exampleEvent = $this->getExampleEvent(); |
||||
$uid = $exampleEvent->getUid(); |
||||
$this->calDavBackend->createCalendarObject( |
||||
$calendarId, |
||||
"$uid.ics", |
||||
$exampleEvent->getIcs(), |
||||
); |
||||
} |
||||
|
||||
private function getStartDate(): \DateTimeInterface { |
||||
return $this->time->now() |
||||
->add(new \DateInterval('P7D')) |
||||
->setTime(10, 00); |
||||
} |
||||
|
||||
private function getEndDate(): \DateTimeInterface { |
||||
return $this->time->now() |
||||
->add(new \DateInterval('P7D')) |
||||
->setTime(11, 00); |
||||
} |
||||
|
||||
private function getDefaultEvent(string $uid): VCalendar { |
||||
$defaultDescription = $this->l10n->t(<<<EOF |
||||
Welcome to Nextcloud Calendar! |
||||
|
||||
This is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want! |
||||
|
||||
With Nextcloud Calendar, you can: |
||||
- Create, edit, and manage events effortlessly. |
||||
- Create multiple calendars and share them with teammates, friends, or family. |
||||
- Check availability and display your busy times to others. |
||||
- Seamlessly integrate with apps and devices via CalDAV. |
||||
- Customize your experience: schedule recurring events, adjust notifications and other settings. |
||||
EOF); |
||||
|
||||
$vCalendar = new VCalendar(); |
||||
$props = [ |
||||
'UID' => $uid, |
||||
'DTSTAMP' => $this->time->now(), |
||||
'SUMMARY' => $this->l10n->t('Example event - open me!'), |
||||
'DTSTART' => $this->getStartDate(), |
||||
'DTEND' => $this->getEndDate(), |
||||
'DESCRIPTION' => $defaultDescription, |
||||
]; |
||||
$vCalendar->add('VEVENT', $props); |
||||
return $vCalendar; |
||||
} |
||||
|
||||
/** |
||||
* @return string|null The ics of the custom example event or null if no custom event was uploaded. |
||||
* @throws ExampleEventException If reading the custom ics file fails. |
||||
*/ |
||||
private function getCustomExampleEvent(): ?string { |
||||
try { |
||||
$folder = $this->appData->getFolder(self::FOLDER_NAME); |
||||
$icsFile = $folder->getFile(self::FILE_NAME); |
||||
} catch (NotFoundException $e) { |
||||
return null; |
||||
} |
||||
|
||||
try { |
||||
return $icsFile->getContent(); |
||||
} catch (NotFoundException|NotPermittedException $e) { |
||||
throw new ExampleEventException( |
||||
'Failed to read custom example event', |
||||
0, |
||||
$e, |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get the configured example event or the default one. |
||||
* |
||||
* @throws ExampleEventException If loading the custom example event fails. |
||||
*/ |
||||
public function getExampleEvent(): ExampleEvent { |
||||
$uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC); |
||||
$customIcs = $this->getCustomExampleEvent(); |
||||
if ($customIcs === null) { |
||||
return new ExampleEvent($this->getDefaultEvent($uid), $uid); |
||||
} |
||||
|
||||
[$vCalendar, $vEvent] = $this->parseEvent($customIcs); |
||||
$vEvent->UID = $uid; |
||||
$vEvent->DTSTART = $this->getStartDate(); |
||||
$vEvent->DTEND = $this->getEndDate(); |
||||
$vEvent->remove('ORGANIZER'); |
||||
$vEvent->remove('ATTENDEE'); |
||||
return new ExampleEvent($vCalendar, $uid); |
||||
} |
||||
|
||||
/** |
||||
* @psalm-return list{VCalendar, VEvent} The VCALENDAR document and its VEVENT child component |
||||
* @throws ExampleEventException If parsing the event fails or if it is invalid. |
||||
*/ |
||||
private function parseEvent(string $ics): array { |
||||
try { |
||||
$vCalendar = \Sabre\VObject\Reader::read($ics); |
||||
if (!($vCalendar instanceof VCalendar)) { |
||||
throw new ExampleEventException('Custom event does not contain a VCALENDAR component'); |
||||
} |
||||
|
||||
/** @var VEvent|null $vEvent */ |
||||
$vEvent = $vCalendar->getBaseComponent('VEVENT'); |
||||
if ($vEvent === null) { |
||||
throw new ExampleEventException('Custom event does not contain a VEVENT component'); |
||||
} |
||||
} catch (\Exception $e) { |
||||
throw new ExampleEventException('Failed to parse custom event: ' . $e->getMessage(), 0, $e); |
||||
} |
||||
|
||||
return [$vCalendar, $vEvent]; |
||||
} |
||||
|
||||
public function saveCustomExampleEvent(string $ics): void { |
||||
// Parse and validate the event before attempting to save it to prevent run time errors |
||||
$this->parseEvent($ics); |
||||
|
||||
try { |
||||
$folder = $this->appData->getFolder(self::FOLDER_NAME); |
||||
} catch (NotFoundException $e) { |
||||
$folder = $this->appData->newFolder(self::FOLDER_NAME); |
||||
} |
||||
|
||||
try { |
||||
$existingFile = $folder->getFile(self::FILE_NAME); |
||||
$existingFile->putContent($ics); |
||||
} catch (NotFoundException $e) { |
||||
$folder->newFile(self::FILE_NAME, $ics); |
||||
} |
||||
} |
||||
|
||||
public function deleteCustomExampleEvent(): void { |
||||
try { |
||||
$folder = $this->appData->getFolder(self::FOLDER_NAME); |
||||
$file = $folder->getFile(self::FILE_NAME); |
||||
} catch (NotFoundException $e) { |
||||
return; |
||||
} |
||||
|
||||
$file->delete(); |
||||
} |
||||
|
||||
public function hasCustomExampleEvent(): bool { |
||||
try { |
||||
return $this->getCustomExampleEvent() !== null; |
||||
} catch (ExampleEventException $e) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
public function setCreateExampleEvent(bool $enable): void { |
||||
$this->appConfig->setValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, $enable); |
||||
} |
||||
|
||||
public function shouldCreateExampleEvent(): bool { |
||||
return $this->appConfig->getValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, true); |
||||
} |
||||
} |
||||
@ -0,0 +1,214 @@ |
||||
<!-- |
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
- SPDX-License-Identifier: AGPL-3.0-or-later |
||||
--> |
||||
|
||||
<template> |
||||
<div class="example-event-settings"> |
||||
<NcCheckboxRadioSwitch :checked="createExampleEvent" |
||||
:disabled="savingConfig" |
||||
type="switch" |
||||
@update:model-value="updateCreateExampleEvent"> |
||||
{{ t('dav', "Add example event to user's calendar when they first log in") }} |
||||
</NcCheckboxRadioSwitch> |
||||
<div v-if="createExampleEvent" |
||||
class="example-event-settings__buttons"> |
||||
<NcButton type="tertiary" |
||||
:href="exampleEventDownloadUrl"> |
||||
<template #icon> |
||||
<IconCalendarBlank :size="20" /> |
||||
</template> |
||||
<span class="example-event-settings__buttons__download-link"> |
||||
example_event.ics |
||||
<IconDownload :size="20" /> |
||||
</span> |
||||
</NcButton> |
||||
<NcButton type="secondary" |
||||
@click="showImportModal = true"> |
||||
<template #icon> |
||||
<IconUpload :size="20" /> |
||||
</template> |
||||
{{ t('dav', 'Import calendar event') }} |
||||
</NcButton> |
||||
<NcButton v-if="hasCustomEvent" |
||||
type="tertiary" |
||||
:disabled="deleting" |
||||
@click="deleteCustomEvent"> |
||||
<template #icon> |
||||
<IconRestore :size="20" /> |
||||
</template> |
||||
{{ t('dav', 'Restore default event') }} |
||||
</NcButton> |
||||
</div> |
||||
<NcDialog :open.sync="showImportModal" |
||||
:name="t('dav', 'Import calendar event')"> |
||||
<div class="import-event-modal"> |
||||
<p> |
||||
{{ t('dav', 'Uploading a new event will overwrite the existing one.') }} |
||||
</p> |
||||
<input ref="event-file" |
||||
:disabled="uploading" |
||||
type="file" |
||||
accept=".ics,text/calendar" |
||||
class="import-event-modal__file-picker" |
||||
@change="selectFile" /> |
||||
<div class="import-event-modal__buttons"> |
||||
<NcButton :disabled="uploading || !selectedFile" |
||||
type="primary" |
||||
@click="uploadCustomEvent()"> |
||||
<template #icon> |
||||
<IconUpload :size="20" /> |
||||
</template> |
||||
{{ t('dav', 'Upload event') }} |
||||
</NcButton> |
||||
</div> |
||||
</div> |
||||
</NcDialog> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { NcButton, NcCheckboxRadioSwitch, NcDialog } from '@nextcloud/vue' |
||||
import { loadState } from '@nextcloud/initial-state' |
||||
import IconDownload from 'vue-material-design-icons/Download.vue' |
||||
import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue' |
||||
import IconUpload from 'vue-material-design-icons/Upload.vue' |
||||
import IconRestore from 'vue-material-design-icons/Restore.vue' |
||||
import * as ExampleEventService from '../service/ExampleEventService.js' |
||||
import { showError, showSuccess } from '@nextcloud/dialogs' |
||||
import logger from '../service/logger.js' |
||||
import { generateUrl } from '@nextcloud/router' |
||||
|
||||
export default { |
||||
name: 'ExampleEventSettings', |
||||
components: { |
||||
NcButton, |
||||
NcCheckboxRadioSwitch, |
||||
NcDialog, |
||||
IconDownload, |
||||
IconCalendarBlank, |
||||
IconUpload, |
||||
IconRestore, |
||||
}, |
||||
data() { |
||||
return { |
||||
createExampleEvent: loadState('dav', 'create_example_event', false), |
||||
hasCustomEvent: loadState('dav', 'has_custom_example_event', false), |
||||
showImportModal: false, |
||||
uploading: false, |
||||
deleting: false, |
||||
savingConfig: false, |
||||
selectedFile: undefined, |
||||
} |
||||
}, |
||||
computed: { |
||||
exampleEventDownloadUrl() { |
||||
return generateUrl('/apps/dav/api/exampleEvent/event') |
||||
}, |
||||
}, |
||||
methods: { |
||||
selectFile() { |
||||
this.selectedFile = this.$refs['event-file']?.files[0] |
||||
}, |
||||
async updateCreateExampleEvent() { |
||||
this.savingConfig = true |
||||
|
||||
const enable = !this.createExampleEvent |
||||
try { |
||||
await ExampleEventService.setCreateExampleEvent(enable) |
||||
} catch (error) { |
||||
showError(t('dav', 'Failed to save example event creation setting')) |
||||
logger.error('Failed to save example event creation setting', { |
||||
error, |
||||
enable, |
||||
}) |
||||
} finally { |
||||
this.savingConfig = false |
||||
} |
||||
|
||||
this.createExampleEvent = enable |
||||
}, |
||||
uploadCustomEvent() { |
||||
if (!this.selectedFile) { |
||||
return |
||||
} |
||||
|
||||
this.uploading = true |
||||
|
||||
const reader = new FileReader() |
||||
reader.addEventListener('load', async () => { |
||||
const ics = reader.result |
||||
|
||||
try { |
||||
await ExampleEventService.uploadExampleEvent(ics) |
||||
} catch (error) { |
||||
showError(t('dav', 'Failed to upload the example event')) |
||||
logger.error('Failed to upload example ICS', { |
||||
error, |
||||
ics, |
||||
}) |
||||
return |
||||
} finally { |
||||
this.uploading = false |
||||
} |
||||
|
||||
showSuccess(t('dav', 'Custom example event was saved successfully')) |
||||
this.showImportModal = false |
||||
this.hasCustomEvent = true |
||||
}) |
||||
reader.readAsText(this.selectedFile) |
||||
}, |
||||
async deleteCustomEvent() { |
||||
this.deleting = true |
||||
|
||||
try { |
||||
await ExampleEventService.deleteExampleEvent() |
||||
} catch (error) { |
||||
showError(t('dav', 'Failed to delete the custom example event')) |
||||
logger.error('Failed to delete the custom example event', { |
||||
error, |
||||
}) |
||||
return |
||||
} finally { |
||||
this.deleting = false |
||||
} |
||||
|
||||
showSuccess(t('dav', 'Custom example event was deleted successfully')) |
||||
this.hasCustomEvent = false |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.example-event-settings { |
||||
margin-block: 2rem; |
||||
|
||||
&__buttons { |
||||
display: flex; |
||||
gap: calc(var(--default-grid-baseline) * 2); |
||||
margin-top: calc(var(--default-grid-baseline) * 2); |
||||
|
||||
&__download-link { |
||||
display: flex; |
||||
text-decoration: underline; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.import-event-modal { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: calc(var(--default-grid-baseline) * 2); |
||||
padding: calc(var(--default-grid-baseline) * 2); |
||||
|
||||
&__file-picker { |
||||
width: 100%; |
||||
} |
||||
|
||||
&__buttons { |
||||
display: flex; |
||||
justify-content: flex-end; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,43 @@ |
||||
/** |
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
|
||||
import { generateUrl } from '@nextcloud/router' |
||||
import axios from '@nextcloud/axios' |
||||
|
||||
/** |
||||
* Configure the creation of example events on a user's first login. |
||||
* |
||||
* @param {boolean} enable Whether to enable or disable the feature. |
||||
* @return {Promise<void>} |
||||
*/ |
||||
export async function setCreateExampleEvent(enable) { |
||||
const url = generateUrl('/apps/dav/api/exampleEvent/enable') |
||||
await axios.post(url, { |
||||
enable, |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Upload a custom example event. |
||||
* |
||||
* @param {string} ics The ICS data of the event. |
||||
* @return {Promise<void>} |
||||
*/ |
||||
export async function uploadExampleEvent(ics) { |
||||
const url = generateUrl('/apps/dav/api/exampleEvent/event') |
||||
await axios.post(url, { |
||||
ics, |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Delete a previously uploaded custom example event. |
||||
* |
||||
* @return {Promise<void>} |
||||
*/ |
||||
export async function deleteExampleEvent() { |
||||
const url = generateUrl('/apps/dav/api/exampleEvent/event') |
||||
await axios.delete(url) |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
<!-- |
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
- SPDX-License-Identifier: AGPL-3.0-or-later |
||||
--> |
||||
|
||||
<template> |
||||
<NcSettingsSection id="example-content" |
||||
:name="$t('dav', 'Example content')" |
||||
class="example-content-setting" |
||||
:description="$t('dav', 'Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content.')"> |
||||
<ExampleContactSettings v-if="hasContactsApp" /> |
||||
<ExampleEventSettings v-if="hasCalendarApp" /> |
||||
</NcSettingsSection> |
||||
</template> |
||||
|
||||
<script> |
||||
import { loadState } from '@nextcloud/initial-state' |
||||
import { NcSettingsSection } from '@nextcloud/vue' |
||||
import ExampleEventSettings from '../components/ExampleEventSettings.vue' |
||||
import ExampleContactSettings from '../components/ExampleContactSettings.vue' |
||||
|
||||
export default { |
||||
name: 'ExampleContentSettingsSection', |
||||
components: { |
||||
NcSettingsSection, |
||||
ExampleContactSettings, |
||||
ExampleEventSettings, |
||||
}, |
||||
computed: { |
||||
hasContactsApp() { |
||||
return loadState('dav', 'contactsEnabled') |
||||
}, |
||||
hasCalendarApp() { |
||||
return loadState('dav', 'calendarEnabled') |
||||
}, |
||||
} |
||||
} |
||||
</script> |
||||
@ -0,0 +1,196 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
|
||||
namespace OCA\DAV\Tests\unit\Service; |
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend; |
||||
use OCA\DAV\Service\ExampleEventService; |
||||
use OCP\AppFramework\Utility\ITimeFactory; |
||||
use OCP\Files\IAppData; |
||||
use OCP\Files\NotFoundException; |
||||
use OCP\Files\SimpleFS\ISimpleFile; |
||||
use OCP\Files\SimpleFS\ISimpleFolder; |
||||
use OCP\IAppConfig; |
||||
use OCP\IL10N; |
||||
use OCP\Security\ISecureRandom; |
||||
use PHPUnit\Framework\MockObject\MockObject; |
||||
use Test\TestCase; |
||||
|
||||
class ExampleEventServiceTest extends TestCase { |
||||
private ExampleEventService $service; |
||||
|
||||
private CalDavBackend&MockObject $calDavBackend; |
||||
private ISecureRandom&MockObject $random; |
||||
private ITimeFactory&MockObject $time; |
||||
private IAppData&MockObject $appData; |
||||
private IAppConfig&MockObject $appConfig; |
||||
private IL10N&MockObject $l10n; |
||||
|
||||
protected function setUp(): void { |
||||
parent::setUp(); |
||||
|
||||
$this->calDavBackend = $this->createMock(CalDavBackend::class); |
||||
$this->random = $this->createMock(ISecureRandom::class); |
||||
$this->time = $this->createMock(ITimeFactory::class); |
||||
$this->appData = $this->createMock(IAppData::class); |
||||
$this->appConfig = $this->createMock(IAppConfig::class); |
||||
$this->l10n = $this->createMock(IL10N::class); |
||||
|
||||
$this->l10n->method('t') |
||||
->willReturnArgument(0); |
||||
|
||||
$this->service = new ExampleEventService( |
||||
$this->calDavBackend, |
||||
$this->random, |
||||
$this->time, |
||||
$this->appData, |
||||
$this->appConfig, |
||||
$this->l10n, |
||||
); |
||||
} |
||||
|
||||
public static function provideCustomEventData(): array { |
||||
return [ |
||||
[file_get_contents(__DIR__ . '/../test_fixtures/example-event.ics')], |
||||
[file_get_contents(__DIR__ . '/../test_fixtures/example-event-with-attendees.ics')], |
||||
]; |
||||
} |
||||
|
||||
/** @dataProvider provideCustomEventData */ |
||||
public function testCreateExampleEventWithCustomEvent($customEventIcs): void { |
||||
$this->appConfig->expects(self::once()) |
||||
->method('getValueBool') |
||||
->with('dav', 'create_example_event', true) |
||||
->willReturn(true); |
||||
|
||||
$exampleEventFolder = $this->createMock(ISimpleFolder::class); |
||||
$this->appData->expects(self::once()) |
||||
->method('getFolder') |
||||
->with('example_event') |
||||
->willReturn($exampleEventFolder); |
||||
$exampleEventFile = $this->createMock(ISimpleFile::class); |
||||
$exampleEventFolder->expects(self::once()) |
||||
->method('getFile') |
||||
->with('example_event.ics') |
||||
->willReturn($exampleEventFile); |
||||
$exampleEventFile->expects(self::once()) |
||||
->method('getContent') |
||||
->willReturn($customEventIcs); |
||||
|
||||
$this->random->expects(self::once()) |
||||
->method('generate') |
||||
->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') |
||||
->willReturn('RANDOM-UID'); |
||||
|
||||
$now = new \DateTimeImmutable('2025-01-21T00:00:00Z'); |
||||
$this->time->expects(self::exactly(2)) |
||||
->method('now') |
||||
->willReturn($now); |
||||
|
||||
$expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics'); |
||||
$this->calDavBackend->expects(self::once()) |
||||
->method('createCalendarObject') |
||||
->with(1000, 'RANDOM-UID.ics', $expectedIcs); |
||||
|
||||
$this->service->createExampleEvent(1000); |
||||
} |
||||
|
||||
public function testCreateExampleEventWithDefaultEvent(): void { |
||||
$this->appConfig->expects(self::once()) |
||||
->method('getValueBool') |
||||
->with('dav', 'create_example_event', true) |
||||
->willReturn(true); |
||||
|
||||
$this->appData->expects(self::once()) |
||||
->method('getFolder') |
||||
->with('example_event') |
||||
->willThrowException(new NotFoundException()); |
||||
|
||||
$this->random->expects(self::once()) |
||||
->method('generate') |
||||
->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') |
||||
->willReturn('RANDOM-UID'); |
||||
|
||||
$now = new \DateTimeImmutable('2025-01-21T00:00:00Z'); |
||||
$this->time->expects(self::exactly(3)) |
||||
->method('now') |
||||
->willReturn($now); |
||||
|
||||
$expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics'); |
||||
$this->calDavBackend->expects(self::once()) |
||||
->method('createCalendarObject') |
||||
->with(1000, 'RANDOM-UID.ics', $expectedIcs); |
||||
|
||||
$this->service->createExampleEvent(1000); |
||||
} |
||||
|
||||
public function testCreateExampleWhenDisabled(): void { |
||||
$this->appConfig->expects(self::once()) |
||||
->method('getValueBool') |
||||
->with('dav', 'create_example_event', true) |
||||
->willReturn(false); |
||||
|
||||
$this->calDavBackend->expects(self::never()) |
||||
->method('createCalendarObject'); |
||||
|
||||
$this->service->createExampleEvent(1000); |
||||
} |
||||
|
||||
/** @dataProvider provideCustomEventData */ |
||||
public function testGetExampleEventWithCustomEvent($customEventIcs): void { |
||||
$exampleEventFolder = $this->createMock(ISimpleFolder::class); |
||||
$this->appData->expects(self::once()) |
||||
->method('getFolder') |
||||
->with('example_event') |
||||
->willReturn($exampleEventFolder); |
||||
$exampleEventFile = $this->createMock(ISimpleFile::class); |
||||
$exampleEventFolder->expects(self::once()) |
||||
->method('getFile') |
||||
->with('example_event.ics') |
||||
->willReturn($exampleEventFile); |
||||
$exampleEventFile->expects(self::once()) |
||||
->method('getContent') |
||||
->willReturn($customEventIcs); |
||||
|
||||
$this->random->expects(self::once()) |
||||
->method('generate') |
||||
->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') |
||||
->willReturn('RANDOM-UID'); |
||||
|
||||
$now = new \DateTimeImmutable('2025-01-21T00:00:00Z'); |
||||
$this->time->expects(self::exactly(2)) |
||||
->method('now') |
||||
->willReturn($now); |
||||
|
||||
$expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics'); |
||||
$actualIcs = $this->service->getExampleEvent()->getIcs(); |
||||
$this->assertEquals($expectedIcs, $actualIcs); |
||||
} |
||||
|
||||
public function testGetExampleEventWithDefault(): void { |
||||
$this->appData->expects(self::once()) |
||||
->method('getFolder') |
||||
->with('example_event') |
||||
->willThrowException(new NotFoundException()); |
||||
|
||||
$this->random->expects(self::once()) |
||||
->method('generate') |
||||
->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') |
||||
->willReturn('RANDOM-UID'); |
||||
|
||||
$now = new \DateTimeImmutable('2025-01-21T00:00:00Z'); |
||||
$this->time->expects(self::exactly(3)) |
||||
->method('now') |
||||
->willReturn($now); |
||||
|
||||
$expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics'); |
||||
$actualIcs = $this->service->getExampleEvent()->getIcs(); |
||||
$this->assertEquals($expectedIcs, $actualIcs); |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@ |
||||
BEGIN:VCALENDAR |
||||
VERSION:2.0 |
||||
PRODID:-//Sabre//Sabre VObject 4.5.6//EN |
||||
CALSCALE:GREGORIAN |
||||
BEGIN:VEVENT |
||||
UID:RANDOM-UID |
||||
DTSTAMP:20250121T000000Z |
||||
SUMMARY:Example event - open me! |
||||
DTSTART:20250128T100000Z |
||||
DTEND:20250128T110000Z |
||||
DESCRIPTION:Welcome to Nextcloud Calendar!\n\nThis is a sample event - expl |
||||
ore the flexibility of planning with Nextcloud Calendar by making any edit |
||||
s you want!\n\nWith Nextcloud Calendar\, you can:\n- Create\, edit\, and m |
||||
anage events effortlessly.\n- Create multiple calendars and share them wit |
||||
h teammates\, friends\, or family.\n- Check availability and display your |
||||
busy times to others.\n- Seamlessly integrate with apps and devices via Ca |
||||
lDAV.\n- Customize your experience: schedule recurring events\, adjust not |
||||
ifications and other settings. |
||||
END:VEVENT |
||||
END:VCALENDAR |
||||
@ -0,0 +1,2 @@ |
||||
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
SPDX-License-Identifier: AGPL-3.0-or-later |
||||
@ -0,0 +1,18 @@ |
||||
BEGIN:VCALENDAR |
||||
VERSION:2.0 |
||||
CALSCALE:GREGORIAN |
||||
PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN |
||||
BEGIN:VEVENT |
||||
CREATED:20250128T091147Z |
||||
DTSTAMP:20250128T091507Z |
||||
LAST-MODIFIED:20250128T091507Z |
||||
SEQUENCE:2 |
||||
STATUS:CONFIRMED |
||||
SUMMARY:Welcome! |
||||
DESCRIPTION:Welcome!!! |
||||
LOCATION:Test |
||||
UID:RANDOM-UID |
||||
DTSTART:20250128T100000Z |
||||
DTEND:20250128T110000Z |
||||
END:VEVENT |
||||
END:VCALENDAR |
||||
@ -0,0 +1,2 @@ |
||||
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
SPDX-License-Identifier: AGPL-3.0-or-later |
||||
@ -0,0 +1,21 @@ |
||||
BEGIN:VCALENDAR |
||||
VERSION:2.0 |
||||
CALSCALE:GREGORIAN |
||||
PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN |
||||
BEGIN:VEVENT |
||||
CREATED:20250128T091147Z |
||||
DTSTAMP:20250128T091507Z |
||||
LAST-MODIFIED:20250128T091507Z |
||||
SEQUENCE:2 |
||||
UID:3b4df6a8-84df-43d5-baf9-377b43390b70 |
||||
DTSTART;VALUE=DATE:20250130 |
||||
DTEND;VALUE=DATE:20250131 |
||||
STATUS:CONFIRMED |
||||
SUMMARY:Welcome! |
||||
DESCRIPTION:Welcome!!! |
||||
LOCATION:Test |
||||
ATTENDEE;CN=user a;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICI |
||||
PANT;RSVP=TRUE;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:usera@imap.localhost |
||||
ORGANIZER;CN=Admin Account:mailto:admin@imap.localhost |
||||
END:VEVENT |
||||
END:VCALENDAR |
||||
@ -0,0 +1,2 @@ |
||||
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
SPDX-License-Identifier: AGPL-3.0-or-later |
||||
@ -0,0 +1,18 @@ |
||||
BEGIN:VCALENDAR |
||||
VERSION:2.0 |
||||
CALSCALE:GREGORIAN |
||||
PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN |
||||
BEGIN:VEVENT |
||||
CREATED:20250128T091147Z |
||||
DTSTAMP:20250128T091507Z |
||||
LAST-MODIFIED:20250128T091507Z |
||||
SEQUENCE:2 |
||||
UID:3b4df6a8-84df-43d5-baf9-377b43390b70 |
||||
STATUS:CONFIRMED |
||||
SUMMARY:Welcome! |
||||
DESCRIPTION:Welcome!!! |
||||
LOCATION:Test |
||||
DTSTART:20250204T100000Z |
||||
DTEND:20250204T110000Z |
||||
END:VEVENT |
||||
END:VCALENDAR |
||||
@ -0,0 +1,2 @@ |
||||
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
||||
SPDX-License-Identifier: AGPL-3.0-or-later |
||||
Loading…
Reference in new issue