feat: create example event when a user logs in for the first time
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